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/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,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
- 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
+ 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
- currentVersion: string;
318
- latestVersion: string | null;
319
- updateAvailable: boolean;
320
- checkedAt: number;
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
- id: string;
326
- type: "info" | "warning" | "critical";
327
- title: string;
328
- message: string;
329
- createdAt: string;
330
- actionUrl?: string | null;
331
- actionLabel?: string | null;
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
- 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[];
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
- 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;
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
- 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;
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
- modelKey: string;
437
- policy: RoutingPolicy;
438
- selectedEmail: string | null;
439
- reason: string;
440
- availableCandidates: number;
441
- rejectedCandidates: number;
442
- accounts: RoutingAccountDiagnostic[];
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
- timestamp: number;
448
- source: "rotator" | "proxy";
449
- level: "info" | "warn" | "error";
450
- message: string;
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
- timestamp: number;
455
- model: string;
456
- account: string;
457
- statusCode: number;
458
- ttfbMs: number;
459
- totalMs: number;
460
- inputTokens: number;
461
- outputTokens: number;
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
- 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 }>;
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
- 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
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
- 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
- };
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
- string,
498
- {
499
- inputPer1M: number;
500
- outputPer1M: number;
501
- cachingPer1M?: number;
502
- cachingStoragePer1MPerHour?: number;
503
- }
572
+ string,
573
+ {
574
+ inputPer1M: number;
575
+ outputPer1M: number;
576
+ cachingPer1M?: number;
577
+ cachingStoragePer1MPerHour?: number;
578
+ }
504
579
  > = {
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 },
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
- "MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
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
- "https://daily-cloudcode-pa.googleapis.com",
527
- // "https://cloudcode-pa.googleapis.com",
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 = "https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
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 = 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";
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
- 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
639
+ process.env.PI_ROTATOR_CLIENT_METADATA ||
640
+ JSON.stringify({
641
+ ideType: "ANTIGRAVITY",
642
+ platform: "MACOS",
643
+ pluginType: "GEMINI",
644
+ });