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/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 = "timer-first" | "tier-first" | "quota-first" | "hybrid";
5
+ export type RoutingPolicy =
6
+ | "timer-first"
7
+ | "tier-first"
8
+ | "quota-first"
9
+ | "hybrid";
6
10
 
7
11
  export type RoutingRejectionReason =
8
- | "disabled"
9
- | "flagged"
10
- | "account-concurrency"
11
- | "project-concurrency"
12
- | "cooldown"
13
- | "fresh-window-blocked"
14
- | "quota-zero"
15
- | "project-breaker"
16
- | "model-breaker"
17
- | "daily-account-stop"
18
- | "daily-project-stop"
19
- | "token-bucket-empty";
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
- email: string;
23
- refreshToken: string;
24
- projectId: string;
25
- // How the projectId was obtained.
26
- projectSource?: "google" | "manual";
27
- label?: string;
28
- // Optional - pro/free is detected dynamically from quota API reset times
29
- type?: AccountType;
30
- tier?: AccountTier;
31
- familyManager?: boolean;
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
- accounts: AccountConfig[];
36
- requestsPerRotation: number;
37
- proxyPort: number;
38
- bindHost?: string;
39
- routingPolicy?: RoutingPolicy;
40
- // Rotate when a model's quota drops by this many percentage points (0 = disabled, use request count)
41
- rotateOnQuotaDrop: number;
42
- // How often to poll quota (ms). Default: 5min
43
- quotaPollIntervalMs: number;
44
- // Hard cap on parallel requests per account. Conservative default is 1.
45
- maxConcurrentRequestsPerAccount?: number;
46
- // Hard cap on parallel requests per projectId/model. Conservative default is 1.
47
- maxConcurrentRequestsPerProjectModel?: number;
48
- // Global delay in ms added to every request to slow down traffic and avoid rate limits.
49
- globalRequestDelayMs?: number;
50
- // Pause projectId/model when several accounts hit provider 429 in a short window. Defaults: 3 hits / 10min / 60min pause.
51
- projectCircuitBreaker429Threshold?: number;
52
- projectCircuitBreakerWindowMs?: number;
53
- projectCircuitBreakerCooldownMs?: number;
54
- // Pause a model globally when several unique accounts hit provider 429 in a short window. Defaults: 3 hits / same window / 6h pause.
55
- modelCircuitBreaker429Threshold?: number;
56
- modelCircuitBreakerCooldownMs?: number;
57
- // Daily safety budgets. Defaults: account slow 250, account stop 350, project slow 900, project stop 1200.
58
- dailyAccountSlowRequests?: number;
59
- dailyAccountStopRequests?: number;
60
- dailyProjectSlowRequests?: number;
61
- dailyProjectStopRequests?: number;
62
- // Add small delay before upstream call when an account/project is in slow mode. Default: 8-25s.
63
- slowModeJitterMinMs?: number;
64
- slowModeJitterMaxMs?: number;
65
- // Pause all routing after a serious provider flag. Default: 6h.
66
- protectivePauseMs?: number;
67
- // Use request-count rotation only before quota data is available. Default: true.
68
- useRequestCountRotationWhenQuotaUnknownOnly?: boolean;
69
- tokenBucketEnabled?: boolean;
70
- tokenBucketMaxTokens?: number;
71
- tokenBucketRefillPerMinute?: number;
72
- tokenBucketInitialTokens?: number;
73
- // Override per-model specs used by the compat layer. Keys are model id substrings
74
- // matched case-insensitively. When set, replaces the bundled defaults entirely.
75
- modelSpecs?: Record<string, ModelSpecConfig>;
76
- // Override model-id aliases used to translate the operator-facing name
77
- // (e.g. "gemini-3.5-flash-high") to the upstream Antigravity name
78
- // (e.g. "gemini-3-flash-agent"). When set, replaces the bundled defaults.
79
- modelAliases?: Record<string, string>;
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
- "gemini-3.1-pro-high": "gemini-pro-agent",
88
- "gemini-3.5-flash": "gemini-3-flash-agent",
89
- "gemini-3.5-flash-high": "gemini-3-flash-agent",
90
- "gemini-3.5-flash-medium": "gemini-3-flash-agent",
91
- "gpt-oss-120b": "gpt-oss-120b-medium",
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(aliases: Record<string, string> | null): void {
104
- modelAliasesOverride = aliases && Object.keys(aliases).length > 0 ? aliases : null;
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
- return modelAliasesOverride ?? DEFAULT_MODEL_ALIASES;
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
- const aliases = getActiveModelAliases();
117
- if (model in aliases) return aliases[model];
118
- const lower = model.toLowerCase();
119
- for (const [from, to] of Object.entries(aliases)) {
120
- if (from.toLowerCase() === lower) return to;
121
- }
122
- return model;
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
- models: Record<
128
- string,
129
- {
130
- quotaInfo?: {
131
- remainingFraction?: number;
132
- resetTime?: string;
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
- maxOutputTokens: number;
142
- thinkingBudget: number; // -1 = adaptive (model decides), >=0 = fixed
143
- isThinking: boolean;
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
- maxOutputTokens: number;
150
- thinkingBudget: number; // -1 = adaptive (model decides), >=0 = fixed
151
- isThinking: boolean;
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
- modelKey: string;
157
- displayName: string;
158
- percentRemaining: number;
159
- resetTime: string | null;
160
- // Timer classification based on resetTime duration
161
- // "fresh" = no active timer, "5h" = short timer, "7d" = long timer
162
- timerType: "fresh" | "5h" | "7d";
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<string, { key: string; altKeys: string[]; display: string }> = {
167
- claude: {
168
- key: "claude-opus-4-6-thinking",
169
- 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", "gpt-oss-120b-medium", "gpt-oss-120b"],
170
- display: "Claude",
171
- },
172
- "gemini-3.1-pro": {
173
- key: "gemini-3.1-pro",
174
- altKeys: ["gemini-3.1-pro-high", "gemini-3.1-pro-low", "gemini-3-pro-high", "gemini-3-pro-low"],
175
- display: "G3.1Pro",
176
- },
177
- "gemini-3.5-flash": {
178
- key: "gemini-3.5-flash",
179
- altKeys: ["gemini-3.5-flash-low", "gemini-3.5-flash-medium", "gemini-3.5-flash-high", "gemini-3-flash-agent", "gemini-3-flash"],
180
- display: "G3.5Flash",
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
- const lower = requestModel.toLowerCase();
187
- // Explicit mappings to avoid substring collisions
188
- if (lower.includes("gemini-3-flash-agent")) return "gemini-3.5-flash";
189
- if (lower.includes("gpt-oss")) return "claude-opus-4-6-thinking";
190
-
191
- for (const [, config] of Object.entries(QUOTA_MODEL_KEYS)) {
192
- if (lower.includes(config.key) || config.altKeys.some((alt) => lower.includes(alt))) {
193
- return config.key;
194
- }
195
- }
196
- // Broad fallback matching
197
- if (lower.includes("gemini") && lower.includes("3.5") && lower.includes("flash")) return "gemini-3.5-flash";
198
- if (lower.includes("gemini") && lower.includes("pro")) return "gemini-3.1-pro";
199
- if (lower.includes("gemini") && lower.includes("flash")) return "gemini-3.5-flash";
200
- if (lower.includes("claude")) return "claude-opus-4-6-thinking";
201
- return null;
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
- const lower = requestModel.toLowerCase();
212
- // Explicit agent and gpt-oss overrides
213
- if (lower.includes("gemini-3-flash-agent")) return "gemini-3.5-flash-high";
214
- if (lower.includes("gpt-oss-120b")) return "gpt-oss-120b-medium";
215
-
216
- // Claude — distinguish sonnet vs opus
217
- if (lower.includes("claude")) {
218
- if (lower.includes("sonnet")) return "claude-sonnet-4-6";
219
- if (lower.includes("opus")) return "claude-opus-4-6-thinking";
220
- return "claude-opus-4-6-thinking"; // fallback
221
- }
222
- // Gemini Pro — distinguish low vs high
223
- if (lower.includes("gemini") && lower.includes("pro")) {
224
- if (lower.includes("-low")) return "gemini-3.1-pro-low";
225
- if (lower.includes("-high")) return "gemini-3.1-pro-high";
226
- return "gemini-3.1-pro"; // unspecified variant
227
- }
228
- // Gemini 3.5 Flash — distinguish medium vs high
229
- if (lower.includes("gemini") && lower.includes("3.5") && lower.includes("flash")) {
230
- if (lower.includes("-low") || lower.includes("-medium")) return "gemini-3.5-flash-medium";
231
- if (lower.includes("-high")) return "gemini-3.5-flash-high";
232
- return "gemini-3.5-flash"; // unspecified variant
233
- }
234
- // Flash
235
- if (lower.includes("gemini") && lower.includes("flash")) return "gemini-3-flash";
236
- // Fallback: return as-is cleaned up
237
- return requestModel;
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
- config: AccountConfig;
243
- accessToken: string | null;
244
- tokenExpires: number;
245
- // Rotation tracking (per-model via rotator)
246
- requestsSinceRotation: number;
247
- totalRequests: number;
248
- // Cooldown / exhaustion per-model
249
- cooldownsByModel: Record<string, number>;
250
- quotaExhaustedAt: number;
251
- // Quota tracking (from API) - per-model data
252
- quota: ModelQuota[];
253
- lastQuotaPoll: number;
254
- // Status
255
- lastUsed: number;
256
- lastError: string | null;
257
- consecutiveErrors: number;
258
- disabled: boolean; // permanently disabled (revoked token, etc.)
259
- flagged: boolean; // flagged for infringement/abuse by Google
260
- inFlightRequests: number;
261
- inFlightByModel: Record<string, number>;
262
- allowFreshWindowStartsOverride: boolean;
263
- dailyRequestCount: number;
264
- dailyRequestDay: string;
265
- healthScore: number;
266
- tokenBucket: {
267
- tokens: number;
268
- lastRefillAt: number;
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
- activeAccountIndex: number;
275
- quotaAtRotationStart: number; // quota % when this account became active for this model
276
- requestsOnActiveAccount: number;
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
- day: string;
282
- projectRequests: Record<string, number>;
283
- projectModelBreakers: Record<string, number>;
284
- modelBreakers?: Record<string, number>;
285
- provider429Events: Array<{ ts: number; projectId: string; modelKey: string; account: string }>;
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
- // Per-model active account index
290
- modelAccounts: Record<string, number>;
291
- // Per-model request count on the active account
292
- modelRequestCounts?: Record<string, number>;
293
- // Legacy fallback
294
- currentIndex?: number;
295
- protectivePauseUntil?: number;
296
- protectivePauseReason?: string | null;
297
- allowFreshWindowStarts?: boolean;
298
- safety?: PersistedSafetyState;
299
- accounts: Record<
300
- string,
301
- {
302
- totalRequests: number;
303
- dailyRequestCount?: number;
304
- dailyRequestDay?: string;
305
- cooldownUntil?: number; // legacy fallback
306
- cooldownsByModel?: Record<string, number>;
307
- quotaExhaustedAt: number;
308
- disabled: boolean;
309
- flagged: boolean;
310
- allowFreshWindowStartsOverride?: boolean;
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
- currentVersion: string;
318
- latestVersion: string | null;
319
- updateAvailable: boolean;
320
- checkedAt: number;
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
- id: string;
326
- type: "info" | "warning" | "critical";
327
- title: string;
328
- message: string;
329
- createdAt: string;
330
- actionUrl?: string | null;
331
- actionLabel?: string | null;
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
- version: string;
337
- proxyPort: number;
338
- requestsPerRotation: number;
339
- totalRequestsAllAccounts: number;
340
- uptime: number;
341
- // Per-model active account
342
- activeAccounts: Record<string, string>;
343
- accounts: AccountStatus[];
344
- protectivePauseUntil: number;
345
- protectivePauseRemaining: number;
346
- protectivePauseReason: string | null;
347
- operatorControls: {
348
- allowFreshWindowStarts: boolean;
349
- };
350
- security: {
351
- adminTokenConfigured: boolean;
352
- warning: string | null;
353
- bindHost: string;
354
- };
355
- routingDiagnostics: Record<string, RoutingModelDiagnostics>;
356
- circuitBreakers: {
357
- model: Record<string, { until: number; remainingMs: number }>;
358
- project: Record<string, { until: number; remainingMs: number }>;
359
- };
360
- routingHealth: {
361
- state: "healthy" | "paused" | "cooldown_wait" | "busy" | "stopped";
362
- reason: string;
363
- nextRetryIn: number;
364
- availableCount: number;
365
- readyCount: number;
366
- activeCount: number;
367
- cooldownCount: number;
368
- busyCount: number;
369
- flaggedCount: number;
370
- disabledCount: number;
371
- errorCount: number;
372
- };
373
- recentEvents: RecentEvent[];
374
- requestLog: RequestLogEntry[];
375
- tokenUsage: TokenUsageData;
376
- latencyStats: Record<string, { ttfb: { p50: number; p95: number }; total: { p50: number; p95: number }; count: number }>;
377
- updateInfo?: UpdateInfo;
378
- notifications?: AdminNotification[];
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
- email: string;
383
- label: string;
384
- status: "active" | "ready" | "cooldown" | "exhausted" | "disabled" | "flagged" | "error";
385
- // Which models this account is currently active for
386
- activeForModels: string[];
387
- requestsSinceRotation: number;
388
- totalRequests: number;
389
- dailyRequestCount: number;
390
- dailyAccountStopRequests: number;
391
- dailyProjectRequestCount: number;
392
- dailyProjectStopRequests: number;
393
- cooldownsByModel: Record<string, number>;
394
- lastUsed: number;
395
- lastError: string | null;
396
- consecutiveErrors: number;
397
- hasValidToken: boolean;
398
- quota: ModelQuota[];
399
- inFlightRequests: number;
400
- inFlightByModel: Record<string, number>;
401
- // Pro family sharing
402
- proDetected: boolean;
403
- tier: AccountTier;
404
- healthScore: number;
405
- tokenBucket: {
406
- enabled: boolean;
407
- tokens: number;
408
- capacity: number;
409
- nextRefillInMs: number;
410
- };
411
- allowFreshWindowStartsOverride: boolean;
412
- effectiveFreshWindowStartsAllowed: boolean;
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
- email: string;
417
- label: string;
418
- status: AccountStatus["status"];
419
- score: number | null;
420
- timerPriority: number | null;
421
- quota: number | null;
422
- tier: AccountTier;
423
- healthScore: number;
424
- distance: number | null;
425
- tokenBucket: {
426
- enabled: boolean;
427
- tokens: number;
428
- capacity: number;
429
- nextRefillInMs: number;
430
- };
431
- rejectedReason: RoutingRejectionReason | null;
432
- rejectedDetail: string | null;
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
- modelKey: string;
437
- policy: RoutingPolicy;
438
- selectedEmail: string | null;
439
- reason: string;
440
- availableCandidates: number;
441
- rejectedCandidates: number;
442
- accounts: RoutingAccountDiagnostic[];
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
- timestamp: number;
448
- source: "rotator" | "proxy";
449
- level: "info" | "warn" | "error";
450
- message: string;
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
- timestamp: number;
455
- model: string;
456
- account: string;
457
- statusCode: number;
458
- ttfbMs: number;
459
- totalMs: number;
460
- inputTokens: number;
461
- outputTokens: number;
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
- period: string; // key: "2026-04-28T12:05" (min), "2026-04-28T12" (hour), "2026-04-28" (day), "2026-04" (month)
467
- inputTokens: number;
468
- outputTokens: number;
469
- requests: number;
470
- byModel: Record<string, { inputTokens: number; outputTokens: number; requests: number }>;
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
- minutes: TokenBucket[]; // current hour, max 60
475
- hours: TokenBucket[]; // last 7 days, max 168
476
- days: TokenBucket[]; // last year, max 365
477
- months: TokenBucket[]; // historical, unlimited
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
- minutes: TokenBucket[];
482
- hours: TokenBucket[];
483
- days: TokenBucket[];
484
- months: TokenBucket[];
485
- totalInputTokens: number;
486
- totalOutputTokens: number;
487
- totalRequests: number;
488
- tokensByModel: Record<string, { input: number; output: number; requests: number }>;
489
- savings: {
490
- totalUsd: number;
491
- byModel: Record<string, { inputUsd: number; outputUsd: number; totalUsd: number }>;
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
- string,
498
- {
499
- inputPer1M: number;
500
- outputPer1M: number;
501
- cachingPer1M?: number;
502
- cachingStoragePer1MPerHour?: number;
503
- }
570
+ string,
571
+ {
572
+ inputPer1M: number;
573
+ outputPer1M: number;
574
+ cachingPer1M?: number;
575
+ cachingStoragePer1MPerHour?: number;
576
+ }
504
577
  > = {
505
- "claude-opus-4-6-thinking": { inputPer1M: 5.00, outputPer1M: 25.00 },
506
- "claude-sonnet-4-6": { inputPer1M: 3.00, outputPer1M: 15.00 },
507
- "gemini-3.1-pro": { inputPer1M: 2.00, outputPer1M: 12.00 },
508
- "gemini-3.1-pro-low": { inputPer1M: 2.00, outputPer1M: 12.00 },
509
- "gemini-3.1-pro-high": { inputPer1M: 2.00, outputPer1M: 12.00 },
510
- "gemini-3-flash": { inputPer1M: 0.50, outputPer1M: 3.00 },
511
- "gemini-3.5-flash": { inputPer1M: 1.50, outputPer1M: 9.00, cachingPer1M: 0.15, cachingStoragePer1MPerHour: 1.00 },
512
- "gemini-3.5-flash-low": { inputPer1M: 1.50, outputPer1M: 9.00, cachingPer1M: 0.15, cachingStoragePer1MPerHour: 1.00 },
513
- "gemini-3.5-flash-medium": { inputPer1M: 1.50, outputPer1M: 9.00, cachingPer1M: 0.15, cachingStoragePer1MPerHour: 1.00 },
514
- "gemini-3.5-flash-high": { inputPer1M: 1.50, outputPer1M: 9.00, cachingPer1M: 0.15, cachingStoragePer1MPerHour: 1.00 },
515
- "gpt-oss-120b-medium": { inputPer1M: 2.00, outputPer1M: 10.00 },
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
- "MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
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
- "https://daily-cloudcode-pa.googleapis.com",
527
- // "https://cloudcode-pa.googleapis.com",
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 = "https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
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 = process.env.PI_ROTATOR_QUOTA_USER_AGENT || `antigravity/${ANTIGRAVITY_VERSION} darwin/arm64`;
533
- export const REQUEST_USER_AGENT = process.env.PI_ROTATOR_REQUEST_USER_AGENT || QUOTA_USER_AGENT;
534
- export const REQUEST_GOOG_API_CLIENT = process.env.PI_ROTATOR_X_GOOG_API_CLIENT || "google-cloud-sdk vscode_cloudshelleditor/0.1";
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
- process.env.PI_ROTATOR_CLIENT_METADATA || JSON.stringify({ ideType: "ANTIGRAVITY", platform: "MACOS", pluginType: "GEMINI" });
537
-
538
- export const LONG_TIMER_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
637
+ process.env.PI_ROTATOR_CLIENT_METADATA ||
638
+ JSON.stringify({
639
+ ideType: "ANTIGRAVITY",
640
+ platform: "MACOS",
641
+ pluginType: "GEMINI",
642
+ });