pi-antigravity-rotator 2.2.2 → 2.3.1
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 +16 -0
- package/README.md +10 -2
- package/package.json +2 -2
- 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 +346 -3194
- 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 +1780 -1270
- package/src/responses-store.ts +150 -178
- package/src/rotator.ts +3302 -2310
- package/src/settings-repository.ts +314 -0
- package/src/static/dashboard.css +1224 -0
- package/src/static/dashboard.js +2872 -0
- package/src/types.ts +503 -397
- package/tools/telemetry-receiver/receiver.js +5 -0
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,397 @@ 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
|
+
autoWarmupEnabled?: boolean;
|
|
349
|
+
safety?: PersistedSafetyState;
|
|
350
|
+
accounts: Record<
|
|
351
|
+
string,
|
|
352
|
+
{
|
|
353
|
+
totalRequests: number;
|
|
354
|
+
dailyRequestCount?: number;
|
|
355
|
+
dailyRequestDay?: string;
|
|
356
|
+
cooldownUntil?: number; // legacy fallback
|
|
357
|
+
cooldownsByModel?: Record<string, number>;
|
|
358
|
+
quotaExhaustedAt: number;
|
|
359
|
+
disabled: boolean;
|
|
360
|
+
flagged: boolean;
|
|
361
|
+
allowFreshWindowStartsOverride?: boolean;
|
|
362
|
+
}
|
|
363
|
+
>;
|
|
313
364
|
}
|
|
314
365
|
|
|
315
366
|
// Version check info for dashboard
|
|
316
367
|
export interface UpdateInfo {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
368
|
+
currentVersion: string;
|
|
369
|
+
latestVersion: string | null;
|
|
370
|
+
updateAvailable: boolean;
|
|
371
|
+
checkedAt: number;
|
|
321
372
|
}
|
|
322
373
|
|
|
323
374
|
// Admin broadcast notification
|
|
324
375
|
export interface AdminNotification {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
376
|
+
id: string;
|
|
377
|
+
type: "info" | "warning" | "critical";
|
|
378
|
+
title: string;
|
|
379
|
+
message: string;
|
|
380
|
+
createdAt: string;
|
|
381
|
+
actionUrl?: string | null;
|
|
382
|
+
actionLabel?: string | null;
|
|
332
383
|
}
|
|
333
384
|
|
|
334
385
|
// Dashboard API response
|
|
335
386
|
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
|
-
|
|
387
|
+
version: string;
|
|
388
|
+
proxyPort: number;
|
|
389
|
+
requestsPerRotation: number;
|
|
390
|
+
totalRequestsAllAccounts: number;
|
|
391
|
+
uptime: number;
|
|
392
|
+
// Per-model active account
|
|
393
|
+
activeAccounts: Record<string, string>;
|
|
394
|
+
accounts: AccountStatus[];
|
|
395
|
+
protectivePauseUntil: number;
|
|
396
|
+
protectivePauseRemaining: number;
|
|
397
|
+
protectivePauseReason: string | null;
|
|
398
|
+
operatorControls: {
|
|
399
|
+
allowFreshWindowStarts: boolean;
|
|
400
|
+
autoWarmupEnabled: boolean;
|
|
401
|
+
};
|
|
402
|
+
security: {
|
|
403
|
+
adminTokenConfigured: boolean;
|
|
404
|
+
warning: string | null;
|
|
405
|
+
bindHost: string;
|
|
406
|
+
};
|
|
407
|
+
routingDiagnostics: Record<string, RoutingModelDiagnostics>;
|
|
408
|
+
circuitBreakers: {
|
|
409
|
+
model: Record<string, { until: number; remainingMs: number }>;
|
|
410
|
+
project: Record<string, { until: number; remainingMs: number }>;
|
|
411
|
+
};
|
|
412
|
+
routingHealth: {
|
|
413
|
+
state: "healthy" | "paused" | "cooldown_wait" | "busy" | "stopped";
|
|
414
|
+
reason: string;
|
|
415
|
+
nextRetryIn: number;
|
|
416
|
+
availableCount: number;
|
|
417
|
+
readyCount: number;
|
|
418
|
+
activeCount: number;
|
|
419
|
+
cooldownCount: number;
|
|
420
|
+
busyCount: number;
|
|
421
|
+
flaggedCount: number;
|
|
422
|
+
disabledCount: number;
|
|
423
|
+
errorCount: number;
|
|
424
|
+
};
|
|
425
|
+
recentEvents: RecentEvent[];
|
|
426
|
+
requestLog: RequestLogEntry[];
|
|
427
|
+
tokenUsage: TokenUsageData;
|
|
428
|
+
latencyStats: Record<
|
|
429
|
+
string,
|
|
430
|
+
{
|
|
431
|
+
ttfb: { p50: number; p95: number };
|
|
432
|
+
total: { p50: number; p95: number };
|
|
433
|
+
count: number;
|
|
434
|
+
}
|
|
435
|
+
>;
|
|
436
|
+
updateInfo?: UpdateInfo;
|
|
437
|
+
notifications?: AdminNotification[];
|
|
438
|
+
hostedOAuthConfigured?: boolean;
|
|
379
439
|
}
|
|
380
440
|
|
|
381
441
|
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
|
-
|
|
442
|
+
email: string;
|
|
443
|
+
label: string;
|
|
444
|
+
status:
|
|
445
|
+
| "active"
|
|
446
|
+
| "ready"
|
|
447
|
+
| "cooldown"
|
|
448
|
+
| "exhausted"
|
|
449
|
+
| "disabled"
|
|
450
|
+
| "flagged"
|
|
451
|
+
| "error";
|
|
452
|
+
// Which models this account is currently active for
|
|
453
|
+
activeForModels: string[];
|
|
454
|
+
requestsSinceRotation: number;
|
|
455
|
+
totalRequests: number;
|
|
456
|
+
dailyRequestCount: number;
|
|
457
|
+
dailyAccountStopRequests: number;
|
|
458
|
+
dailyProjectRequestCount: number;
|
|
459
|
+
dailyProjectStopRequests: number;
|
|
460
|
+
cooldownsByModel: Record<string, number>;
|
|
461
|
+
lastUsed: number;
|
|
462
|
+
lastError: string | null;
|
|
463
|
+
consecutiveErrors: number;
|
|
464
|
+
hasValidToken: boolean;
|
|
465
|
+
quota: ModelQuota[];
|
|
466
|
+
inFlightRequests: number;
|
|
467
|
+
inFlightByModel: Record<string, number>;
|
|
468
|
+
// Pro family sharing
|
|
469
|
+
proDetected: boolean;
|
|
470
|
+
tier: AccountTier;
|
|
471
|
+
healthScore: number;
|
|
472
|
+
tokenBucket: {
|
|
473
|
+
enabled: boolean;
|
|
474
|
+
tokens: number;
|
|
475
|
+
capacity: number;
|
|
476
|
+
nextRefillInMs: number;
|
|
477
|
+
};
|
|
478
|
+
allowFreshWindowStartsOverride: boolean;
|
|
479
|
+
effectiveFreshWindowStartsAllowed: boolean;
|
|
413
480
|
}
|
|
414
481
|
|
|
415
482
|
export interface RoutingAccountDiagnostic {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
483
|
+
email: string;
|
|
484
|
+
label: string;
|
|
485
|
+
status: AccountStatus["status"];
|
|
486
|
+
score: number | null;
|
|
487
|
+
timerPriority: number | null;
|
|
488
|
+
quota: number | null;
|
|
489
|
+
tier: AccountTier;
|
|
490
|
+
healthScore: number;
|
|
491
|
+
distance: number | null;
|
|
492
|
+
tokenBucket: {
|
|
493
|
+
enabled: boolean;
|
|
494
|
+
tokens: number;
|
|
495
|
+
capacity: number;
|
|
496
|
+
nextRefillInMs: number;
|
|
497
|
+
};
|
|
498
|
+
rejectedReason: RoutingRejectionReason | null;
|
|
499
|
+
rejectedDetail: string | null;
|
|
433
500
|
}
|
|
434
501
|
|
|
435
502
|
export interface RoutingModelDiagnostics {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
503
|
+
modelKey: string;
|
|
504
|
+
policy: RoutingPolicy;
|
|
505
|
+
selectedEmail: string | null;
|
|
506
|
+
reason: string;
|
|
507
|
+
availableCandidates: number;
|
|
508
|
+
rejectedCandidates: number;
|
|
509
|
+
accounts: RoutingAccountDiagnostic[];
|
|
443
510
|
}
|
|
444
511
|
|
|
445
|
-
|
|
446
512
|
export interface RecentEvent {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
513
|
+
timestamp: number;
|
|
514
|
+
source: "rotator" | "proxy";
|
|
515
|
+
level: "info" | "warn" | "error";
|
|
516
|
+
message: string;
|
|
451
517
|
}
|
|
452
518
|
|
|
453
519
|
export interface RequestLogEntry {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
520
|
+
timestamp: number;
|
|
521
|
+
model: string;
|
|
522
|
+
account: string;
|
|
523
|
+
statusCode: number;
|
|
524
|
+
ttfbMs: number;
|
|
525
|
+
totalMs: number;
|
|
526
|
+
inputTokens: number;
|
|
527
|
+
outputTokens: number;
|
|
462
528
|
}
|
|
463
529
|
|
|
464
530
|
// Token usage tracking — tiered time-series
|
|
465
531
|
export interface TokenBucket {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
532
|
+
period: string; // key: "2026-04-28T12:05" (min), "2026-04-28T12" (hour), "2026-04-28" (day), "2026-04" (month)
|
|
533
|
+
inputTokens: number;
|
|
534
|
+
outputTokens: number;
|
|
535
|
+
requests: number;
|
|
536
|
+
byModel: Record<
|
|
537
|
+
string,
|
|
538
|
+
{ inputTokens: number; outputTokens: number; requests: number }
|
|
539
|
+
>;
|
|
471
540
|
}
|
|
472
541
|
|
|
473
542
|
export interface TokenUsageTiered {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
543
|
+
minutes: TokenBucket[]; // current hour, max 60
|
|
544
|
+
hours: TokenBucket[]; // last 7 days, max 168
|
|
545
|
+
days: TokenBucket[]; // last year, max 365
|
|
546
|
+
months: TokenBucket[]; // historical, unlimited
|
|
478
547
|
}
|
|
479
548
|
|
|
480
549
|
export interface TokenUsageData {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
550
|
+
minutes: TokenBucket[];
|
|
551
|
+
hours: TokenBucket[];
|
|
552
|
+
days: TokenBucket[];
|
|
553
|
+
months: TokenBucket[];
|
|
554
|
+
totalInputTokens: number;
|
|
555
|
+
totalOutputTokens: number;
|
|
556
|
+
totalRequests: number;
|
|
557
|
+
tokensByModel: Record<
|
|
558
|
+
string,
|
|
559
|
+
{ input: number; output: number; requests: number }
|
|
560
|
+
>;
|
|
561
|
+
savings: {
|
|
562
|
+
totalUsd: number;
|
|
563
|
+
byModel: Record<
|
|
564
|
+
string,
|
|
565
|
+
{ inputUsd: number; outputUsd: number; totalUsd: number }
|
|
566
|
+
>;
|
|
567
|
+
};
|
|
493
568
|
}
|
|
494
569
|
|
|
495
570
|
// Pricing per 1M tokens (USD) — what these would cost on paid APIs
|
|
496
571
|
export const MODEL_PRICING: Record<
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
572
|
+
string,
|
|
573
|
+
{
|
|
574
|
+
inputPer1M: number;
|
|
575
|
+
outputPer1M: number;
|
|
576
|
+
cachingPer1M?: number;
|
|
577
|
+
cachingStoragePer1MPerHour?: number;
|
|
578
|
+
}
|
|
504
579
|
> = {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
580
|
+
"claude-opus-4-6-thinking": { inputPer1M: 5.0, outputPer1M: 25.0 },
|
|
581
|
+
"claude-sonnet-4-6": { inputPer1M: 3.0, outputPer1M: 15.0 },
|
|
582
|
+
"gemini-3.1-pro": { inputPer1M: 2.0, outputPer1M: 12.0 },
|
|
583
|
+
"gemini-3.1-pro-low": { inputPer1M: 2.0, outputPer1M: 12.0 },
|
|
584
|
+
"gemini-3.1-pro-high": { inputPer1M: 2.0, outputPer1M: 12.0 },
|
|
585
|
+
"gemini-3-flash": { inputPer1M: 0.5, outputPer1M: 3.0 },
|
|
586
|
+
"gemini-3.5-flash": {
|
|
587
|
+
inputPer1M: 1.5,
|
|
588
|
+
outputPer1M: 9.0,
|
|
589
|
+
cachingPer1M: 0.15,
|
|
590
|
+
cachingStoragePer1MPerHour: 1.0,
|
|
591
|
+
},
|
|
592
|
+
"gemini-3.5-flash-low": {
|
|
593
|
+
inputPer1M: 1.5,
|
|
594
|
+
outputPer1M: 9.0,
|
|
595
|
+
cachingPer1M: 0.15,
|
|
596
|
+
cachingStoragePer1MPerHour: 1.0,
|
|
597
|
+
},
|
|
598
|
+
"gemini-3.5-flash-medium": {
|
|
599
|
+
inputPer1M: 1.5,
|
|
600
|
+
outputPer1M: 9.0,
|
|
601
|
+
cachingPer1M: 0.15,
|
|
602
|
+
cachingStoragePer1MPerHour: 1.0,
|
|
603
|
+
},
|
|
604
|
+
"gemini-3.5-flash-high": {
|
|
605
|
+
inputPer1M: 1.5,
|
|
606
|
+
outputPer1M: 9.0,
|
|
607
|
+
cachingPer1M: 0.15,
|
|
608
|
+
cachingStoragePer1MPerHour: 1.0,
|
|
609
|
+
},
|
|
610
|
+
"gpt-oss-120b-medium": { inputPer1M: 2.0, outputPer1M: 10.0 },
|
|
516
611
|
};
|
|
517
612
|
|
|
518
613
|
// Antigravity OAuth constants (same as pi-mono)
|
|
519
614
|
export const CLIENT_ID = atob(
|
|
520
|
-
|
|
615
|
+
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
|
|
616
|
+
);
|
|
617
|
+
export const CLIENT_SECRET = atob(
|
|
618
|
+
"R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=",
|
|
521
619
|
);
|
|
522
|
-
export const CLIENT_SECRET = atob("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
|
523
620
|
export const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
524
621
|
|
|
525
622
|
export const ANTIGRAVITY_ENDPOINTS = [
|
|
526
|
-
|
|
527
|
-
|
|
623
|
+
"https://daily-cloudcode-pa.googleapis.com",
|
|
624
|
+
// "https://cloudcode-pa.googleapis.com",
|
|
528
625
|
] as const;
|
|
529
626
|
|
|
530
|
-
export const QUOTA_API_URL =
|
|
627
|
+
export const QUOTA_API_URL =
|
|
628
|
+
"https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
|
|
531
629
|
const ANTIGRAVITY_VERSION = process.env.PI_AI_ANTIGRAVITY_VERSION || "1.107.0";
|
|
532
|
-
export const QUOTA_USER_AGENT =
|
|
533
|
-
|
|
534
|
-
|
|
630
|
+
export const QUOTA_USER_AGENT =
|
|
631
|
+
process.env.PI_ROTATOR_QUOTA_USER_AGENT ||
|
|
632
|
+
`antigravity/${ANTIGRAVITY_VERSION} darwin/arm64`;
|
|
633
|
+
export const REQUEST_USER_AGENT =
|
|
634
|
+
process.env.PI_ROTATOR_REQUEST_USER_AGENT || QUOTA_USER_AGENT;
|
|
635
|
+
export const REQUEST_GOOG_API_CLIENT =
|
|
636
|
+
process.env.PI_ROTATOR_X_GOOG_API_CLIENT ||
|
|
637
|
+
"google-cloud-sdk vscode_cloudshelleditor/0.1";
|
|
535
638
|
export const REQUEST_CLIENT_METADATA =
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
639
|
+
process.env.PI_ROTATOR_CLIENT_METADATA ||
|
|
640
|
+
JSON.stringify({
|
|
641
|
+
ideType: "ANTIGRAVITY",
|
|
642
|
+
platform: "MACOS",
|
|
643
|
+
pluginType: "GEMINI",
|
|
644
|
+
});
|