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/types.ts
CHANGED
|
@@ -2,81 +2,85 @@
|
|
|
2
2
|
|
|
3
3
|
export type AccountType = "pro" | "free";
|
|
4
4
|
export type AccountTier = "ultra" | "pro" | "plus" | "free" | "unknown";
|
|
5
|
-
export type RoutingPolicy =
|
|
5
|
+
export type RoutingPolicy =
|
|
6
|
+
| "timer-first"
|
|
7
|
+
| "tier-first"
|
|
8
|
+
| "quota-first"
|
|
9
|
+
| "hybrid";
|
|
6
10
|
|
|
7
11
|
export type RoutingRejectionReason =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
| "disabled"
|
|
13
|
+
| "flagged"
|
|
14
|
+
| "account-concurrency"
|
|
15
|
+
| "project-concurrency"
|
|
16
|
+
| "cooldown"
|
|
17
|
+
| "fresh-window-blocked"
|
|
18
|
+
| "quota-zero"
|
|
19
|
+
| "project-breaker"
|
|
20
|
+
| "model-breaker"
|
|
21
|
+
| "daily-account-stop"
|
|
22
|
+
| "daily-project-stop"
|
|
23
|
+
| "token-bucket-empty";
|
|
20
24
|
|
|
21
25
|
export interface AccountConfig {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
email: string;
|
|
27
|
+
refreshToken: string;
|
|
28
|
+
projectId: string;
|
|
29
|
+
// How the projectId was obtained.
|
|
30
|
+
projectSource?: "google" | "manual";
|
|
31
|
+
label?: string;
|
|
32
|
+
// Optional - pro/free is detected dynamically from quota API reset times
|
|
33
|
+
type?: AccountType;
|
|
34
|
+
tier?: AccountTier;
|
|
35
|
+
familyManager?: boolean;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export interface Config {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
79
|
-
|
|
39
|
+
accounts: AccountConfig[];
|
|
40
|
+
requestsPerRotation: number;
|
|
41
|
+
proxyPort: number;
|
|
42
|
+
bindHost?: string;
|
|
43
|
+
routingPolicy?: RoutingPolicy;
|
|
44
|
+
// Rotate when a model's quota drops by this many percentage points (0 = disabled, use request count)
|
|
45
|
+
rotateOnQuotaDrop: number;
|
|
46
|
+
// How often to poll quota (ms). Default: 5min
|
|
47
|
+
quotaPollIntervalMs: number;
|
|
48
|
+
// Hard cap on parallel requests per account. Conservative default is 1.
|
|
49
|
+
maxConcurrentRequestsPerAccount?: number;
|
|
50
|
+
// Hard cap on parallel requests per projectId/model. Conservative default is 1.
|
|
51
|
+
maxConcurrentRequestsPerProjectModel?: number;
|
|
52
|
+
// Global delay in ms added to every request to slow down traffic and avoid rate limits.
|
|
53
|
+
globalRequestDelayMs?: number;
|
|
54
|
+
// Pause projectId/model when several accounts hit provider 429 in a short window. Defaults: 3 hits / 10min / 60min pause.
|
|
55
|
+
projectCircuitBreaker429Threshold?: number;
|
|
56
|
+
projectCircuitBreakerWindowMs?: number;
|
|
57
|
+
projectCircuitBreakerCooldownMs?: number;
|
|
58
|
+
// Pause a model globally when several unique accounts hit provider 429 in a short window. Defaults: 3 hits / same window / 6h pause.
|
|
59
|
+
modelCircuitBreaker429Threshold?: number;
|
|
60
|
+
modelCircuitBreakerCooldownMs?: number;
|
|
61
|
+
// Daily safety budgets. Defaults: account slow 250, account stop 350, project slow 900, project stop 1200.
|
|
62
|
+
dailyAccountSlowRequests?: number;
|
|
63
|
+
dailyAccountStopRequests?: number;
|
|
64
|
+
dailyProjectSlowRequests?: number;
|
|
65
|
+
dailyProjectStopRequests?: number;
|
|
66
|
+
// Add small delay before upstream call when an account/project is in slow mode. Default: 8-25s.
|
|
67
|
+
slowModeJitterMinMs?: number;
|
|
68
|
+
slowModeJitterMaxMs?: number;
|
|
69
|
+
// Pause all routing after a serious provider flag. Default: 6h.
|
|
70
|
+
protectivePauseMs?: number;
|
|
71
|
+
// Use request-count rotation only before quota data is available. Default: true.
|
|
72
|
+
useRequestCountRotationWhenQuotaUnknownOnly?: boolean;
|
|
73
|
+
tokenBucketEnabled?: boolean;
|
|
74
|
+
tokenBucketMaxTokens?: number;
|
|
75
|
+
tokenBucketRefillPerMinute?: number;
|
|
76
|
+
tokenBucketInitialTokens?: number;
|
|
77
|
+
// Override per-model specs used by the compat layer. Keys are model id substrings
|
|
78
|
+
// matched case-insensitively. When set, replaces the bundled defaults entirely.
|
|
79
|
+
modelSpecs?: Record<string, ModelSpecConfig>;
|
|
80
|
+
// Override model-id aliases used to translate the operator-facing name
|
|
81
|
+
// (e.g. "gemini-3.5-flash-high") to the upstream Antigravity name
|
|
82
|
+
// (e.g. "gemini-3-flash-agent"). When set, replaces the bundled defaults.
|
|
83
|
+
modelAliases?: Record<string, string>;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
// ── Default model-id aliases ─────────────────────────────────────────
|
|
@@ -84,11 +88,11 @@ export interface Config {
|
|
|
84
88
|
// When the provider adds a new model, this is the only place that needs
|
|
85
89
|
// updating. Operators can override via Config.modelAliases in accounts.json.
|
|
86
90
|
const DEFAULT_MODEL_ALIASES: Record<string, string> = {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
"gemini-3.1-pro-high": "gemini-pro-agent",
|
|
92
|
+
"gemini-3.5-flash": "gemini-3-flash-agent",
|
|
93
|
+
"gemini-3.5-flash-high": "gemini-3-flash-agent",
|
|
94
|
+
"gemini-3.5-flash-medium": "gemini-3-flash-agent",
|
|
95
|
+
"gpt-oss-120b": "gpt-oss-120b-medium",
|
|
92
96
|
};
|
|
93
97
|
let modelAliasesOverride: Record<string, string> | null = null;
|
|
94
98
|
|
|
@@ -100,12 +104,15 @@ let modelAliasesOverride: Record<string, string> | null = null;
|
|
|
100
104
|
* @param aliases Map of operator-facing model name to upstream model name,
|
|
101
105
|
* or null to restore defaults.
|
|
102
106
|
*/
|
|
103
|
-
export function setModelAliasesOverride(
|
|
104
|
-
|
|
107
|
+
export function setModelAliasesOverride(
|
|
108
|
+
aliases: Record<string, string> | null,
|
|
109
|
+
): void {
|
|
110
|
+
modelAliasesOverride =
|
|
111
|
+
aliases && Object.keys(aliases).length > 0 ? aliases : null;
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
function getActiveModelAliases(): Record<string, string> {
|
|
108
|
-
|
|
115
|
+
return modelAliasesOverride ?? DEFAULT_MODEL_ALIASES;
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
/**
|
|
@@ -113,92 +120,125 @@ function getActiveModelAliases(): Record<string, string> {
|
|
|
113
120
|
* (case-insensitive). Returns the original model if no alias is configured.
|
|
114
121
|
*/
|
|
115
122
|
export function applyModelAlias(model: string): string {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
const aliases = getActiveModelAliases();
|
|
124
|
+
if (model in aliases) return aliases[model];
|
|
125
|
+
const lower = model.toLowerCase();
|
|
126
|
+
for (const [from, to] of Object.entries(aliases)) {
|
|
127
|
+
if (from.toLowerCase() === lower) return to;
|
|
128
|
+
}
|
|
129
|
+
return model;
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
// Quota API response from Google
|
|
126
133
|
export interface GoogleQuotaResponse {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
models: Record<
|
|
135
|
+
string,
|
|
136
|
+
{
|
|
137
|
+
quotaInfo?: {
|
|
138
|
+
remainingFraction?: number;
|
|
139
|
+
resetTime?: string;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
>;
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
// Per-model thinking/output spec used by the compat layer.
|
|
139
146
|
// Operators can override defaults via the `modelSpecs` field in accounts.json.
|
|
140
147
|
export interface ModelSpecConfig {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
maxOutputTokens: number;
|
|
149
|
+
thinkingBudget: number; // -1 = adaptive (model decides), >=0 = fixed
|
|
150
|
+
isThinking: boolean;
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
// Per-model thinking/output spec used by the compat layer.
|
|
147
154
|
// Operators can override defaults via the `modelSpecs` field in accounts.json.
|
|
148
155
|
export interface ModelSpecConfig {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
maxOutputTokens: number;
|
|
157
|
+
thinkingBudget: number; // -1 = adaptive (model decides), >=0 = fixed
|
|
158
|
+
isThinking: boolean;
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
// Per-model quota info for an account
|
|
155
162
|
export interface ModelQuota {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
modelKey: string;
|
|
164
|
+
displayName: string;
|
|
165
|
+
percentRemaining: number;
|
|
166
|
+
resetTime: string | null;
|
|
167
|
+
// Timer classification based on resetTime duration
|
|
168
|
+
// "fresh" = no active timer, "5h" = short timer, "7d" = long timer
|
|
169
|
+
timerType: "fresh" | "5h" | "7d";
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
// Model key mapping for the quota API
|
|
166
|
-
export const QUOTA_MODEL_KEYS: Record<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
173
|
+
export const QUOTA_MODEL_KEYS: Record<
|
|
174
|
+
string,
|
|
175
|
+
{ key: string; altKeys: string[]; display: string }
|
|
176
|
+
> = {
|
|
177
|
+
claude: {
|
|
178
|
+
key: "claude-opus-4-6-thinking",
|
|
179
|
+
altKeys: [
|
|
180
|
+
"claude-opus-4-5-thinking",
|
|
181
|
+
"claude-opus-4-5",
|
|
182
|
+
"claude-sonnet-4-6-thinking",
|
|
183
|
+
"claude-sonnet-4-6",
|
|
184
|
+
"claude-sonnet-4-5-thinking",
|
|
185
|
+
"claude-sonnet-4-5",
|
|
186
|
+
"gpt-oss-120b-medium",
|
|
187
|
+
"gpt-oss-120b",
|
|
188
|
+
],
|
|
189
|
+
display: "Claude",
|
|
190
|
+
},
|
|
191
|
+
"gemini-3.1-pro": {
|
|
192
|
+
key: "gemini-3.1-pro",
|
|
193
|
+
altKeys: [
|
|
194
|
+
"gemini-3.1-pro-high",
|
|
195
|
+
"gemini-3.1-pro-low",
|
|
196
|
+
"gemini-3-pro-high",
|
|
197
|
+
"gemini-3-pro-low",
|
|
198
|
+
],
|
|
199
|
+
display: "G3.1Pro",
|
|
200
|
+
},
|
|
201
|
+
"gemini-3.5-flash": {
|
|
202
|
+
key: "gemini-3.5-flash",
|
|
203
|
+
altKeys: [
|
|
204
|
+
"gemini-3.5-flash-low",
|
|
205
|
+
"gemini-3.5-flash-medium",
|
|
206
|
+
"gemini-3.5-flash-high",
|
|
207
|
+
"gemini-3-flash-agent",
|
|
208
|
+
"gemini-3-flash",
|
|
209
|
+
],
|
|
210
|
+
display: "G3.5Flash",
|
|
211
|
+
},
|
|
182
212
|
};
|
|
183
213
|
|
|
184
214
|
// Map request model names to quota model keys
|
|
185
215
|
export function resolveQuotaModelKey(requestModel: string): string | null {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
216
|
+
const lower = requestModel.toLowerCase();
|
|
217
|
+
// Explicit mappings to avoid substring collisions
|
|
218
|
+
if (lower.includes("gemini-3-flash-agent")) return "gemini-3.5-flash";
|
|
219
|
+
if (lower.includes("gpt-oss")) return "claude-opus-4-6-thinking";
|
|
220
|
+
|
|
221
|
+
for (const [, config] of Object.entries(QUOTA_MODEL_KEYS)) {
|
|
222
|
+
if (
|
|
223
|
+
lower.includes(config.key) ||
|
|
224
|
+
config.altKeys.some((alt) => lower.includes(alt))
|
|
225
|
+
) {
|
|
226
|
+
return config.key;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Broad fallback matching
|
|
230
|
+
if (
|
|
231
|
+
lower.includes("gemini") &&
|
|
232
|
+
lower.includes("3.5") &&
|
|
233
|
+
lower.includes("flash")
|
|
234
|
+
)
|
|
235
|
+
return "gemini-3.5-flash";
|
|
236
|
+
if (lower.includes("gemini") && lower.includes("pro"))
|
|
237
|
+
return "gemini-3.1-pro";
|
|
238
|
+
if (lower.includes("gemini") && lower.includes("flash"))
|
|
239
|
+
return "gemini-3.5-flash";
|
|
240
|
+
if (lower.includes("claude")) return "claude-opus-4-6-thinking";
|
|
241
|
+
return null;
|
|
202
242
|
}
|
|
203
243
|
|
|
204
244
|
/**
|
|
@@ -208,331 +248,395 @@ export function resolveQuotaModelKey(requestModel: string): string | null {
|
|
|
208
248
|
* - claude-sonnet-4-6 vs claude-opus-4-6-thinking (different pricing)
|
|
209
249
|
*/
|
|
210
250
|
export function resolveDisplayModelKey(requestModel: string): string {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
251
|
+
const lower = requestModel.toLowerCase();
|
|
252
|
+
// Explicit agent and gpt-oss overrides
|
|
253
|
+
if (lower.includes("gemini-3-flash-agent")) return "gemini-3.5-flash-high";
|
|
254
|
+
if (lower.includes("gpt-oss-120b")) return "gpt-oss-120b-medium";
|
|
255
|
+
|
|
256
|
+
// Claude — distinguish sonnet vs opus
|
|
257
|
+
if (lower.includes("claude")) {
|
|
258
|
+
if (lower.includes("sonnet")) return "claude-sonnet-4-6";
|
|
259
|
+
if (lower.includes("opus")) return "claude-opus-4-6-thinking";
|
|
260
|
+
return "claude-opus-4-6-thinking"; // fallback
|
|
261
|
+
}
|
|
262
|
+
// Gemini Pro — distinguish low vs high
|
|
263
|
+
if (lower.includes("gemini") && lower.includes("pro")) {
|
|
264
|
+
if (lower.includes("-low")) return "gemini-3.1-pro-low";
|
|
265
|
+
if (lower.includes("-high")) return "gemini-3.1-pro-high";
|
|
266
|
+
return "gemini-3.1-pro"; // unspecified variant
|
|
267
|
+
}
|
|
268
|
+
// Gemini 3.5 Flash — distinguish medium vs high
|
|
269
|
+
if (
|
|
270
|
+
lower.includes("gemini") &&
|
|
271
|
+
lower.includes("3.5") &&
|
|
272
|
+
lower.includes("flash")
|
|
273
|
+
) {
|
|
274
|
+
if (lower.includes("-low") || lower.includes("-medium"))
|
|
275
|
+
return "gemini-3.5-flash-medium";
|
|
276
|
+
if (lower.includes("-high")) return "gemini-3.5-flash-high";
|
|
277
|
+
return "gemini-3.5-flash"; // unspecified variant
|
|
278
|
+
}
|
|
279
|
+
// Flash
|
|
280
|
+
if (lower.includes("gemini") && lower.includes("flash"))
|
|
281
|
+
return "gemini-3-flash";
|
|
282
|
+
// Fallback: return as-is cleaned up
|
|
283
|
+
return requestModel;
|
|
238
284
|
}
|
|
239
285
|
|
|
240
286
|
// Runtime state for a single account
|
|
241
287
|
export interface AccountRuntime {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
288
|
+
config: AccountConfig;
|
|
289
|
+
accessToken: string | null;
|
|
290
|
+
tokenExpires: number;
|
|
291
|
+
// Rotation tracking (per-model via rotator)
|
|
292
|
+
requestsSinceRotation: number;
|
|
293
|
+
totalRequests: number;
|
|
294
|
+
// Cooldown / exhaustion per-model
|
|
295
|
+
cooldownsByModel: Record<string, number>;
|
|
296
|
+
quotaExhaustedAt: number;
|
|
297
|
+
// Quota tracking (from API) - per-model data
|
|
298
|
+
quota: ModelQuota[];
|
|
299
|
+
lastQuotaPoll: number;
|
|
300
|
+
// Status
|
|
301
|
+
lastUsed: number;
|
|
302
|
+
lastError: string | null;
|
|
303
|
+
consecutiveErrors: number;
|
|
304
|
+
disabled: boolean; // permanently disabled (revoked token, etc.)
|
|
305
|
+
flagged: boolean; // flagged for infringement/abuse by Google
|
|
306
|
+
inFlightRequests: number;
|
|
307
|
+
inFlightByModel: Record<string, number>;
|
|
308
|
+
allowFreshWindowStartsOverride: boolean;
|
|
309
|
+
dailyRequestCount: number;
|
|
310
|
+
dailyRequestDay: string;
|
|
311
|
+
healthScore: number;
|
|
312
|
+
tokenBucket: {
|
|
313
|
+
tokens: number;
|
|
314
|
+
lastRefillAt: number;
|
|
315
|
+
};
|
|
270
316
|
}
|
|
271
317
|
|
|
272
318
|
// Per-model rotation state tracked by the rotator
|
|
273
319
|
export interface ModelRotationState {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
320
|
+
activeAccountIndex: number;
|
|
321
|
+
quotaAtRotationStart: number; // quota % when this account became active for this model
|
|
322
|
+
requestsOnActiveAccount: number;
|
|
277
323
|
}
|
|
278
324
|
|
|
279
|
-
|
|
280
325
|
export interface PersistedSafetyState {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
326
|
+
day: string;
|
|
327
|
+
projectRequests: Record<string, number>;
|
|
328
|
+
projectModelBreakers: Record<string, number>;
|
|
329
|
+
modelBreakers?: Record<string, number>;
|
|
330
|
+
provider429Events: Array<{
|
|
331
|
+
ts: number;
|
|
332
|
+
projectId: string;
|
|
333
|
+
modelKey: string;
|
|
334
|
+
account: string;
|
|
335
|
+
}>;
|
|
286
336
|
}
|
|
287
337
|
|
|
288
338
|
export interface PersistedState {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
339
|
+
// Per-model active account index
|
|
340
|
+
modelAccounts: Record<string, number>;
|
|
341
|
+
// Per-model request count on the active account
|
|
342
|
+
modelRequestCounts?: Record<string, number>;
|
|
343
|
+
// Legacy fallback
|
|
344
|
+
currentIndex?: number;
|
|
345
|
+
protectivePauseUntil?: number;
|
|
346
|
+
protectivePauseReason?: string | null;
|
|
347
|
+
allowFreshWindowStarts?: boolean;
|
|
348
|
+
safety?: PersistedSafetyState;
|
|
349
|
+
accounts: Record<
|
|
350
|
+
string,
|
|
351
|
+
{
|
|
352
|
+
totalRequests: number;
|
|
353
|
+
dailyRequestCount?: number;
|
|
354
|
+
dailyRequestDay?: string;
|
|
355
|
+
cooldownUntil?: number; // legacy fallback
|
|
356
|
+
cooldownsByModel?: Record<string, number>;
|
|
357
|
+
quotaExhaustedAt: number;
|
|
358
|
+
disabled: boolean;
|
|
359
|
+
flagged: boolean;
|
|
360
|
+
allowFreshWindowStartsOverride?: boolean;
|
|
361
|
+
}
|
|
362
|
+
>;
|
|
313
363
|
}
|
|
314
364
|
|
|
315
365
|
// Version check info for dashboard
|
|
316
366
|
export interface UpdateInfo {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
367
|
+
currentVersion: string;
|
|
368
|
+
latestVersion: string | null;
|
|
369
|
+
updateAvailable: boolean;
|
|
370
|
+
checkedAt: number;
|
|
321
371
|
}
|
|
322
372
|
|
|
323
373
|
// Admin broadcast notification
|
|
324
374
|
export interface AdminNotification {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
375
|
+
id: string;
|
|
376
|
+
type: "info" | "warning" | "critical";
|
|
377
|
+
title: string;
|
|
378
|
+
message: string;
|
|
379
|
+
createdAt: string;
|
|
380
|
+
actionUrl?: string | null;
|
|
381
|
+
actionLabel?: string | null;
|
|
332
382
|
}
|
|
333
383
|
|
|
334
384
|
// Dashboard API response
|
|
335
385
|
export interface StatusResponse {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
386
|
+
version: string;
|
|
387
|
+
proxyPort: number;
|
|
388
|
+
requestsPerRotation: number;
|
|
389
|
+
totalRequestsAllAccounts: number;
|
|
390
|
+
uptime: number;
|
|
391
|
+
// Per-model active account
|
|
392
|
+
activeAccounts: Record<string, string>;
|
|
393
|
+
accounts: AccountStatus[];
|
|
394
|
+
protectivePauseUntil: number;
|
|
395
|
+
protectivePauseRemaining: number;
|
|
396
|
+
protectivePauseReason: string | null;
|
|
397
|
+
operatorControls: {
|
|
398
|
+
allowFreshWindowStarts: boolean;
|
|
399
|
+
};
|
|
400
|
+
security: {
|
|
401
|
+
adminTokenConfigured: boolean;
|
|
402
|
+
warning: string | null;
|
|
403
|
+
bindHost: string;
|
|
404
|
+
};
|
|
405
|
+
routingDiagnostics: Record<string, RoutingModelDiagnostics>;
|
|
406
|
+
circuitBreakers: {
|
|
407
|
+
model: Record<string, { until: number; remainingMs: number }>;
|
|
408
|
+
project: Record<string, { until: number; remainingMs: number }>;
|
|
409
|
+
};
|
|
410
|
+
routingHealth: {
|
|
411
|
+
state: "healthy" | "paused" | "cooldown_wait" | "busy" | "stopped";
|
|
412
|
+
reason: string;
|
|
413
|
+
nextRetryIn: number;
|
|
414
|
+
availableCount: number;
|
|
415
|
+
readyCount: number;
|
|
416
|
+
activeCount: number;
|
|
417
|
+
cooldownCount: number;
|
|
418
|
+
busyCount: number;
|
|
419
|
+
flaggedCount: number;
|
|
420
|
+
disabledCount: number;
|
|
421
|
+
errorCount: number;
|
|
422
|
+
};
|
|
423
|
+
recentEvents: RecentEvent[];
|
|
424
|
+
requestLog: RequestLogEntry[];
|
|
425
|
+
tokenUsage: TokenUsageData;
|
|
426
|
+
latencyStats: Record<
|
|
427
|
+
string,
|
|
428
|
+
{
|
|
429
|
+
ttfb: { p50: number; p95: number };
|
|
430
|
+
total: { p50: number; p95: number };
|
|
431
|
+
count: number;
|
|
432
|
+
}
|
|
433
|
+
>;
|
|
434
|
+
updateInfo?: UpdateInfo;
|
|
435
|
+
notifications?: AdminNotification[];
|
|
436
|
+
hostedOAuthConfigured?: boolean;
|
|
379
437
|
}
|
|
380
438
|
|
|
381
439
|
export interface AccountStatus {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
440
|
+
email: string;
|
|
441
|
+
label: string;
|
|
442
|
+
status:
|
|
443
|
+
| "active"
|
|
444
|
+
| "ready"
|
|
445
|
+
| "cooldown"
|
|
446
|
+
| "exhausted"
|
|
447
|
+
| "disabled"
|
|
448
|
+
| "flagged"
|
|
449
|
+
| "error";
|
|
450
|
+
// Which models this account is currently active for
|
|
451
|
+
activeForModels: string[];
|
|
452
|
+
requestsSinceRotation: number;
|
|
453
|
+
totalRequests: number;
|
|
454
|
+
dailyRequestCount: number;
|
|
455
|
+
dailyAccountStopRequests: number;
|
|
456
|
+
dailyProjectRequestCount: number;
|
|
457
|
+
dailyProjectStopRequests: number;
|
|
458
|
+
cooldownsByModel: Record<string, number>;
|
|
459
|
+
lastUsed: number;
|
|
460
|
+
lastError: string | null;
|
|
461
|
+
consecutiveErrors: number;
|
|
462
|
+
hasValidToken: boolean;
|
|
463
|
+
quota: ModelQuota[];
|
|
464
|
+
inFlightRequests: number;
|
|
465
|
+
inFlightByModel: Record<string, number>;
|
|
466
|
+
// Pro family sharing
|
|
467
|
+
proDetected: boolean;
|
|
468
|
+
tier: AccountTier;
|
|
469
|
+
healthScore: number;
|
|
470
|
+
tokenBucket: {
|
|
471
|
+
enabled: boolean;
|
|
472
|
+
tokens: number;
|
|
473
|
+
capacity: number;
|
|
474
|
+
nextRefillInMs: number;
|
|
475
|
+
};
|
|
476
|
+
allowFreshWindowStartsOverride: boolean;
|
|
477
|
+
effectiveFreshWindowStartsAllowed: boolean;
|
|
413
478
|
}
|
|
414
479
|
|
|
415
480
|
export interface RoutingAccountDiagnostic {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
481
|
+
email: string;
|
|
482
|
+
label: string;
|
|
483
|
+
status: AccountStatus["status"];
|
|
484
|
+
score: number | null;
|
|
485
|
+
timerPriority: number | null;
|
|
486
|
+
quota: number | null;
|
|
487
|
+
tier: AccountTier;
|
|
488
|
+
healthScore: number;
|
|
489
|
+
distance: number | null;
|
|
490
|
+
tokenBucket: {
|
|
491
|
+
enabled: boolean;
|
|
492
|
+
tokens: number;
|
|
493
|
+
capacity: number;
|
|
494
|
+
nextRefillInMs: number;
|
|
495
|
+
};
|
|
496
|
+
rejectedReason: RoutingRejectionReason | null;
|
|
497
|
+
rejectedDetail: string | null;
|
|
433
498
|
}
|
|
434
499
|
|
|
435
500
|
export interface RoutingModelDiagnostics {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
501
|
+
modelKey: string;
|
|
502
|
+
policy: RoutingPolicy;
|
|
503
|
+
selectedEmail: string | null;
|
|
504
|
+
reason: string;
|
|
505
|
+
availableCandidates: number;
|
|
506
|
+
rejectedCandidates: number;
|
|
507
|
+
accounts: RoutingAccountDiagnostic[];
|
|
443
508
|
}
|
|
444
509
|
|
|
445
|
-
|
|
446
510
|
export interface RecentEvent {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
511
|
+
timestamp: number;
|
|
512
|
+
source: "rotator" | "proxy";
|
|
513
|
+
level: "info" | "warn" | "error";
|
|
514
|
+
message: string;
|
|
451
515
|
}
|
|
452
516
|
|
|
453
517
|
export interface RequestLogEntry {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
518
|
+
timestamp: number;
|
|
519
|
+
model: string;
|
|
520
|
+
account: string;
|
|
521
|
+
statusCode: number;
|
|
522
|
+
ttfbMs: number;
|
|
523
|
+
totalMs: number;
|
|
524
|
+
inputTokens: number;
|
|
525
|
+
outputTokens: number;
|
|
462
526
|
}
|
|
463
527
|
|
|
464
528
|
// Token usage tracking — tiered time-series
|
|
465
529
|
export interface TokenBucket {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
530
|
+
period: string; // key: "2026-04-28T12:05" (min), "2026-04-28T12" (hour), "2026-04-28" (day), "2026-04" (month)
|
|
531
|
+
inputTokens: number;
|
|
532
|
+
outputTokens: number;
|
|
533
|
+
requests: number;
|
|
534
|
+
byModel: Record<
|
|
535
|
+
string,
|
|
536
|
+
{ inputTokens: number; outputTokens: number; requests: number }
|
|
537
|
+
>;
|
|
471
538
|
}
|
|
472
539
|
|
|
473
540
|
export interface TokenUsageTiered {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
541
|
+
minutes: TokenBucket[]; // current hour, max 60
|
|
542
|
+
hours: TokenBucket[]; // last 7 days, max 168
|
|
543
|
+
days: TokenBucket[]; // last year, max 365
|
|
544
|
+
months: TokenBucket[]; // historical, unlimited
|
|
478
545
|
}
|
|
479
546
|
|
|
480
547
|
export interface TokenUsageData {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
548
|
+
minutes: TokenBucket[];
|
|
549
|
+
hours: TokenBucket[];
|
|
550
|
+
days: TokenBucket[];
|
|
551
|
+
months: TokenBucket[];
|
|
552
|
+
totalInputTokens: number;
|
|
553
|
+
totalOutputTokens: number;
|
|
554
|
+
totalRequests: number;
|
|
555
|
+
tokensByModel: Record<
|
|
556
|
+
string,
|
|
557
|
+
{ input: number; output: number; requests: number }
|
|
558
|
+
>;
|
|
559
|
+
savings: {
|
|
560
|
+
totalUsd: number;
|
|
561
|
+
byModel: Record<
|
|
562
|
+
string,
|
|
563
|
+
{ inputUsd: number; outputUsd: number; totalUsd: number }
|
|
564
|
+
>;
|
|
565
|
+
};
|
|
493
566
|
}
|
|
494
567
|
|
|
495
568
|
// Pricing per 1M tokens (USD) — what these would cost on paid APIs
|
|
496
569
|
export const MODEL_PRICING: Record<
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
570
|
+
string,
|
|
571
|
+
{
|
|
572
|
+
inputPer1M: number;
|
|
573
|
+
outputPer1M: number;
|
|
574
|
+
cachingPer1M?: number;
|
|
575
|
+
cachingStoragePer1MPerHour?: number;
|
|
576
|
+
}
|
|
504
577
|
> = {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
578
|
+
"claude-opus-4-6-thinking": { inputPer1M: 5.0, outputPer1M: 25.0 },
|
|
579
|
+
"claude-sonnet-4-6": { inputPer1M: 3.0, outputPer1M: 15.0 },
|
|
580
|
+
"gemini-3.1-pro": { inputPer1M: 2.0, outputPer1M: 12.0 },
|
|
581
|
+
"gemini-3.1-pro-low": { inputPer1M: 2.0, outputPer1M: 12.0 },
|
|
582
|
+
"gemini-3.1-pro-high": { inputPer1M: 2.0, outputPer1M: 12.0 },
|
|
583
|
+
"gemini-3-flash": { inputPer1M: 0.5, outputPer1M: 3.0 },
|
|
584
|
+
"gemini-3.5-flash": {
|
|
585
|
+
inputPer1M: 1.5,
|
|
586
|
+
outputPer1M: 9.0,
|
|
587
|
+
cachingPer1M: 0.15,
|
|
588
|
+
cachingStoragePer1MPerHour: 1.0,
|
|
589
|
+
},
|
|
590
|
+
"gemini-3.5-flash-low": {
|
|
591
|
+
inputPer1M: 1.5,
|
|
592
|
+
outputPer1M: 9.0,
|
|
593
|
+
cachingPer1M: 0.15,
|
|
594
|
+
cachingStoragePer1MPerHour: 1.0,
|
|
595
|
+
},
|
|
596
|
+
"gemini-3.5-flash-medium": {
|
|
597
|
+
inputPer1M: 1.5,
|
|
598
|
+
outputPer1M: 9.0,
|
|
599
|
+
cachingPer1M: 0.15,
|
|
600
|
+
cachingStoragePer1MPerHour: 1.0,
|
|
601
|
+
},
|
|
602
|
+
"gemini-3.5-flash-high": {
|
|
603
|
+
inputPer1M: 1.5,
|
|
604
|
+
outputPer1M: 9.0,
|
|
605
|
+
cachingPer1M: 0.15,
|
|
606
|
+
cachingStoragePer1MPerHour: 1.0,
|
|
607
|
+
},
|
|
608
|
+
"gpt-oss-120b-medium": { inputPer1M: 2.0, outputPer1M: 10.0 },
|
|
516
609
|
};
|
|
517
610
|
|
|
518
611
|
// Antigravity OAuth constants (same as pi-mono)
|
|
519
612
|
export const CLIENT_ID = atob(
|
|
520
|
-
|
|
613
|
+
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
|
|
614
|
+
);
|
|
615
|
+
export const CLIENT_SECRET = atob(
|
|
616
|
+
"R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=",
|
|
521
617
|
);
|
|
522
|
-
export const CLIENT_SECRET = atob("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
|
523
618
|
export const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
524
619
|
|
|
525
620
|
export const ANTIGRAVITY_ENDPOINTS = [
|
|
526
|
-
|
|
527
|
-
|
|
621
|
+
"https://daily-cloudcode-pa.googleapis.com",
|
|
622
|
+
// "https://cloudcode-pa.googleapis.com",
|
|
528
623
|
] as const;
|
|
529
624
|
|
|
530
|
-
export const QUOTA_API_URL =
|
|
625
|
+
export const QUOTA_API_URL =
|
|
626
|
+
"https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
|
|
531
627
|
const ANTIGRAVITY_VERSION = process.env.PI_AI_ANTIGRAVITY_VERSION || "1.107.0";
|
|
532
|
-
export const QUOTA_USER_AGENT =
|
|
533
|
-
|
|
534
|
-
|
|
628
|
+
export const QUOTA_USER_AGENT =
|
|
629
|
+
process.env.PI_ROTATOR_QUOTA_USER_AGENT ||
|
|
630
|
+
`antigravity/${ANTIGRAVITY_VERSION} darwin/arm64`;
|
|
631
|
+
export const REQUEST_USER_AGENT =
|
|
632
|
+
process.env.PI_ROTATOR_REQUEST_USER_AGENT || QUOTA_USER_AGENT;
|
|
633
|
+
export const REQUEST_GOOG_API_CLIENT =
|
|
634
|
+
process.env.PI_ROTATOR_X_GOOG_API_CLIENT ||
|
|
635
|
+
"google-cloud-sdk vscode_cloudshelleditor/0.1";
|
|
535
636
|
export const REQUEST_CLIENT_METADATA =
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
637
|
+
process.env.PI_ROTATOR_CLIENT_METADATA ||
|
|
638
|
+
JSON.stringify({
|
|
639
|
+
ideType: "ANTIGRAVITY",
|
|
640
|
+
platform: "MACOS",
|
|
641
|
+
pluginType: "GEMINI",
|
|
642
|
+
});
|