codex-antigravity-auth 1.3.0__py3-none-any.whl

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.
@@ -0,0 +1,15 @@
1
+ """Codex Antigravity Auth package."""
2
+
3
+ __all__ = ["app", "main"]
4
+
5
+
6
+ def __getattr__(name):
7
+ if name == "app":
8
+ from .server import app
9
+
10
+ return app
11
+ if name == "main":
12
+ from .cli import main
13
+
14
+ return main
15
+ raise AttributeError(name)
@@ -0,0 +1,435 @@
1
+ import math
2
+ import time
3
+ import threading
4
+ from typing import Any
5
+ from .storage import load_accounts, get_accounts_json_path, update_accounts
6
+ from .oauth import refresh_access_token, token_expires_in_seconds
7
+ from .fingerprint import generate_fingerprint
8
+ from .redaction import redact_secret_text
9
+
10
+ class AccountManager:
11
+ def __init__(self):
12
+ self._lock = threading.RLock()
13
+ self._failures = {} # email -> failure count
14
+ self._cooldowns = {} # email -> cooldown end timestamp
15
+ self._counters = {} # email -> family -> sanitized usage counters
16
+
17
+ def _sync_state_from_storage(self, data: dict[str, Any]) -> bool:
18
+ state_missing = "accountState" not in data
19
+ state = data.get("accountState", {})
20
+ previous_state = state if isinstance(state, dict) else {}
21
+ accounts = data.get("accounts", [])
22
+ account_emails = {
23
+ str(account.get("email"))
24
+ for account in accounts
25
+ if isinstance(account, dict) and account.get("email")
26
+ }
27
+ if isinstance(state, dict):
28
+ failures = state.get("failures", {})
29
+ cooldowns = state.get("cooldowns", {})
30
+ counters = state.get("counters", {})
31
+ if state_missing:
32
+ if self._failures:
33
+ failures = {
34
+ **(failures if isinstance(failures, dict) else {}),
35
+ **self._failures,
36
+ }
37
+ if self._cooldowns:
38
+ cooldowns = {
39
+ **(cooldowns if isinstance(cooldowns, dict) else {}),
40
+ **self._cooldowns,
41
+ }
42
+ if self._counters:
43
+ counters = {
44
+ **(counters if isinstance(counters, dict) else {}),
45
+ **self._counters,
46
+ }
47
+ active_cooldown_emails = set()
48
+ cleaned_cooldowns = {}
49
+ if isinstance(cooldowns, dict):
50
+ current_time = time.time()
51
+ for k, v in cooldowns.items():
52
+ email = str(k)
53
+ if email not in account_emails:
54
+ continue
55
+ if not isinstance(v, (int, float)) or isinstance(v, bool):
56
+ continue
57
+ cooldown_end = float(v)
58
+ if math.isfinite(cooldown_end) and cooldown_end > current_time:
59
+ cleaned_cooldowns[email] = cooldown_end
60
+ active_cooldown_emails.add(email)
61
+ self._cooldowns = cleaned_cooldowns
62
+ cleaned_failures = {}
63
+ if isinstance(failures, dict):
64
+ for k, v in failures.items():
65
+ email = str(k)
66
+ if email not in active_cooldown_emails:
67
+ continue
68
+ if not isinstance(v, (int, float)) or isinstance(v, bool):
69
+ continue
70
+ failure_number = float(v)
71
+ if not math.isfinite(failure_number):
72
+ continue
73
+ failure_count = int(failure_number)
74
+ if failure_count > 0:
75
+ cleaned_failures[email] = failure_count
76
+ self._failures = cleaned_failures
77
+ cleaned_counters: dict[str, dict[str, dict[str, Any]]] = {}
78
+ if isinstance(counters, dict):
79
+ for email_key, raw_families in counters.items():
80
+ email = str(email_key)
81
+ if email not in account_emails or not isinstance(raw_families, dict):
82
+ continue
83
+ family_counters: dict[str, dict[str, Any]] = {}
84
+ for family_key, raw_counter in raw_families.items():
85
+ family = str(family_key)
86
+ if family not in {"claude", "gemini"} or not isinstance(raw_counter, dict):
87
+ continue
88
+ family_counters[family] = self._sanitize_counter(raw_counter)
89
+ if family_counters:
90
+ cleaned_counters[email] = family_counters
91
+ self._counters = cleaned_counters
92
+ else:
93
+ self._failures = {}
94
+ self._cooldowns = {}
95
+ self._counters = {}
96
+ cleaned_state = {
97
+ "failures": self._failures,
98
+ "cooldowns": self._cooldowns,
99
+ "counters": self._counters,
100
+ }
101
+ return previous_state != cleaned_state
102
+
103
+ def _save_state_to_storage(self) -> None:
104
+ if not get_accounts_json_path().exists():
105
+ return
106
+
107
+ def mutate(data: dict[str, Any]) -> None:
108
+ data["accountState"] = {
109
+ "failures": self._failures,
110
+ "cooldowns": self._cooldowns,
111
+ "counters": self._counters,
112
+ }
113
+
114
+ update_accounts(mutate)
115
+
116
+ @staticmethod
117
+ def _sanitize_counter(raw_counter: dict[str, Any]) -> dict[str, Any]:
118
+ sanitized: dict[str, Any] = {}
119
+ integer_fields = (
120
+ "total_requests",
121
+ "successes",
122
+ "failures",
123
+ "rate_limits",
124
+ "input_tokens",
125
+ "output_tokens",
126
+ "total_tokens",
127
+ )
128
+ for field in integer_fields:
129
+ value = raw_counter.get(field, 0)
130
+ if isinstance(value, bool):
131
+ value = 0
132
+ try:
133
+ parsed = int(value)
134
+ except (TypeError, ValueError):
135
+ parsed = 0
136
+ sanitized[field] = max(0, parsed)
137
+ for field in ("last_success", "last_failure", "last_failure_class"):
138
+ value = raw_counter.get(field)
139
+ if isinstance(value, str) and not any(ord(ch) < 0x20 or ord(ch) == 0x7F for ch in value):
140
+ sanitized[field] = redact_secret_text(value)[:200]
141
+ return sanitized
142
+
143
+ @staticmethod
144
+ def _model_family(model: str) -> str:
145
+ return "claude" if "claude" in str(model).lower() else "gemini"
146
+
147
+ def _counter_for(self, email: str, family: str) -> dict[str, Any]:
148
+ account_counters = self._counters.setdefault(email, {})
149
+ counter = account_counters.setdefault(
150
+ family,
151
+ {
152
+ "total_requests": 0,
153
+ "successes": 0,
154
+ "failures": 0,
155
+ "rate_limits": 0,
156
+ "input_tokens": 0,
157
+ "output_tokens": 0,
158
+ "total_tokens": 0,
159
+ },
160
+ )
161
+ account_counters[family] = self._sanitize_counter(counter)
162
+ return account_counters[family]
163
+
164
+ @staticmethod
165
+ def _normalize_expires_at(value: Any) -> float:
166
+ try:
167
+ expires_at = float(value or 0)
168
+ except (TypeError, ValueError):
169
+ return 0
170
+ if not math.isfinite(expires_at):
171
+ return 0
172
+ # Epoch milliseconds are currently around 1.7e12; epoch seconds around 1.7e9.
173
+ if expires_at > 10_000_000_000:
174
+ expires_at = expires_at / 1000
175
+ return expires_at
176
+
177
+ def get_accounts(self) -> list[dict[str, Any]]:
178
+ with self._lock:
179
+ data = load_accounts()
180
+ return data.get("accounts", [])
181
+
182
+ def select_active_account(self, model: str) -> dict[str, Any] | None:
183
+ with self._lock:
184
+ selected: dict[str, Any] | None = None
185
+
186
+ def mutate(data: dict[str, Any]) -> bool:
187
+ nonlocal selected
188
+ dirty = self._sync_state_from_storage(data)
189
+ accounts = data.get("accounts", [])
190
+ if not accounts:
191
+ if dirty:
192
+ data["accountState"] = {
193
+ "failures": self._failures,
194
+ "cooldowns": self._cooldowns,
195
+ }
196
+ return dirty
197
+
198
+ family = "claude" if "claude" in model.lower() else "gemini"
199
+ family_map = data.setdefault("activeIndexByFamily", {"claude": 0, "gemini": 0})
200
+
201
+ # Simple rotation/selection strategy:
202
+ # Check accounts from the preferred active index for this family.
203
+ start_index = family_map.get(family, 0)
204
+ if not isinstance(start_index, int) or start_index < 0 or start_index >= len(accounts):
205
+ start_index = 0
206
+
207
+ current_time = time.time()
208
+ for i in range(len(accounts)):
209
+ idx = (start_index + i) % len(accounts)
210
+ acc = accounts[idx]
211
+ email = acc.get("email")
212
+ if not email:
213
+ continue
214
+
215
+ cooldown_end = self._cooldowns.get(email, 0)
216
+ if cooldown_end > current_time:
217
+ continue
218
+ if cooldown_end:
219
+ self._cooldowns.pop(email, None)
220
+ self._failures.pop(email, None)
221
+ dirty = True
222
+
223
+ if not acc.get("fingerprint"):
224
+ acc["fingerprint"] = generate_fingerprint()
225
+ dirty = True
226
+
227
+ expires_at = self._normalize_expires_at(acc.get("expiresAt", 0))
228
+ if expires_at != acc.get("expiresAt", 0):
229
+ acc["expiresAt"] = expires_at
230
+ dirty = True
231
+ if not acc.get("accessToken") or expires_at < current_time + 300:
232
+ refresh_tok = acc.get("refreshToken")
233
+ if refresh_tok:
234
+ try:
235
+ refreshed = refresh_access_token(refresh_tok)
236
+ acc["accessToken"] = refreshed["access_token"]
237
+ acc["expiresAt"] = current_time + token_expires_in_seconds(refreshed)
238
+ if refreshed.get("refresh_token"):
239
+ acc["refreshToken"] = refreshed["refresh_token"]
240
+ dirty = True
241
+ except Exception as e:
242
+ self._record_failure(email, retry_after_seconds=None)
243
+ print(f"[*] Account {email} flagged as cooling down. Reason: {redact_secret_text(f'Token refresh failed: {e}')}")
244
+ dirty = True
245
+ continue
246
+ else:
247
+ self._record_failure(email, retry_after_seconds=None)
248
+ print(f"[*] Account {email} flagged as cooling down. Reason: Token expired and no refresh token is available")
249
+ dirty = True
250
+ continue
251
+
252
+ if family_map.get(family) != idx:
253
+ dirty = True
254
+ family_map[family] = idx
255
+ if data.get("activeIndex") != idx:
256
+ dirty = True
257
+ data["activeIndex"] = idx
258
+ state_payload = {
259
+ "failures": self._failures,
260
+ "cooldowns": self._cooldowns,
261
+ "counters": self._counters,
262
+ }
263
+ if self._failures or self._cooldowns or self._counters or data.get("accountState"):
264
+ if data.get("accountState") != state_payload:
265
+ dirty = True
266
+ data["accountState"] = state_payload
267
+ selected = acc
268
+ return dirty
269
+ if dirty:
270
+ data["accountState"] = {
271
+ "failures": self._failures,
272
+ "cooldowns": self._cooldowns,
273
+ "counters": self._counters,
274
+ }
275
+ return dirty
276
+
277
+ update_accounts(mutate)
278
+ return selected
279
+
280
+ def _record_failure(self, email: str, retry_after_seconds: float | None = None) -> float:
281
+ previous_failures = self._failures.get(email, 0)
282
+ if not isinstance(previous_failures, int) or isinstance(previous_failures, bool) or previous_failures < 0:
283
+ previous_failures = 0
284
+ self._failures[email] = previous_failures + 1
285
+ backoff_factor = min(self._failures[email], 5)
286
+ cooldown_duration = 120 * (2 ** (backoff_factor - 1))
287
+ try:
288
+ retry_after = (
289
+ float(retry_after_seconds)
290
+ if retry_after_seconds is not None and not isinstance(retry_after_seconds, bool)
291
+ else 0.0
292
+ )
293
+ except (TypeError, ValueError):
294
+ retry_after = 0.0
295
+ if math.isfinite(retry_after) and retry_after > 0:
296
+ cooldown_duration = max(cooldown_duration, min(retry_after, 86_400.0))
297
+ self._cooldowns[email] = time.time() + cooldown_duration
298
+ return cooldown_duration
299
+
300
+ def mark_failure(
301
+ self,
302
+ email: str,
303
+ reason: str,
304
+ retry_after_seconds: float | None = None,
305
+ *,
306
+ model: str | None = None,
307
+ status_code: int | None = None,
308
+ ) -> None:
309
+ with self._lock:
310
+ if not email:
311
+ return
312
+ cooldown_duration = self._record_failure(email, retry_after_seconds)
313
+ self._save_state_to_storage()
314
+
315
+ # Print warning
316
+ print(f"[*] Account {email} flagged as cooling down for {cooldown_duration}s. Reason: {redact_secret_text(reason)}")
317
+
318
+ def record_request(
319
+ self,
320
+ email: str,
321
+ model: str,
322
+ *,
323
+ status: str,
324
+ status_code: int | None = None,
325
+ error_class: str | None = None,
326
+ usage: dict[str, Any] | None = None,
327
+ ) -> None:
328
+ with self._lock:
329
+ self._record_request_locked(
330
+ email,
331
+ model,
332
+ status=status,
333
+ status_code=status_code,
334
+ error_class=error_class,
335
+ usage=usage,
336
+ persist=True,
337
+ )
338
+
339
+ def _record_request_locked(
340
+ self,
341
+ email: str,
342
+ model: str,
343
+ *,
344
+ status: str,
345
+ status_code: int | None = None,
346
+ error_class: str | None = None,
347
+ usage: dict[str, Any] | None = None,
348
+ persist: bool,
349
+ ) -> None:
350
+ if not email:
351
+ return
352
+ family = self._model_family(model)
353
+ counter = self._counter_for(email, family)
354
+ counter["total_requests"] = int(counter.get("total_requests", 0)) + 1
355
+ now_text = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
356
+ if status == "success":
357
+ counter["successes"] = int(counter.get("successes", 0)) + 1
358
+ counter["last_success"] = now_text
359
+ else:
360
+ counter["failures"] = int(counter.get("failures", 0)) + 1
361
+ if status_code == 429:
362
+ counter["rate_limits"] = int(counter.get("rate_limits", 0)) + 1
363
+ counter["last_failure"] = now_text
364
+ if error_class:
365
+ counter["last_failure_class"] = redact_secret_text(str(error_class))[:200]
366
+ if usage:
367
+ for usage_field, counter_field in (
368
+ ("input_tokens", "input_tokens"),
369
+ ("output_tokens", "output_tokens"),
370
+ ("total_tokens", "total_tokens"),
371
+ ):
372
+ value = usage.get(usage_field)
373
+ if isinstance(value, bool):
374
+ continue
375
+ try:
376
+ parsed = int(value)
377
+ except (TypeError, ValueError):
378
+ continue
379
+ if parsed > 0:
380
+ counter[counter_field] = int(counter.get(counter_field, 0)) + parsed
381
+ self._counters[email][family] = self._sanitize_counter(counter)
382
+ if persist:
383
+ self._save_state_to_storage()
384
+
385
+ def refresh_expiring_accounts(self, window_seconds: int = 300) -> dict[str, int]:
386
+ with self._lock:
387
+ summary = {"checked": 0, "refreshed": 0, "failed": 0}
388
+ if not get_accounts_json_path().exists():
389
+ return summary
390
+ current_time = time.time()
391
+
392
+ def mutate(data: dict[str, Any]) -> bool:
393
+ dirty = self._sync_state_from_storage(data)
394
+ accounts = data.get("accounts", [])
395
+ if not isinstance(accounts, list):
396
+ return dirty
397
+ for acc in accounts:
398
+ if not isinstance(acc, dict):
399
+ continue
400
+ email = acc.get("email")
401
+ refresh_tok = acc.get("refreshToken")
402
+ if not email or not refresh_tok:
403
+ continue
404
+ summary["checked"] += 1
405
+ expires_at = self._normalize_expires_at(acc.get("expiresAt", 0))
406
+ if expires_at > current_time + max(0, int(window_seconds)):
407
+ continue
408
+ try:
409
+ refreshed = refresh_access_token(refresh_tok)
410
+ acc["accessToken"] = refreshed["access_token"]
411
+ acc["expiresAt"] = current_time + token_expires_in_seconds(refreshed)
412
+ if refreshed.get("refresh_token"):
413
+ acc["refreshToken"] = refreshed["refresh_token"]
414
+ summary["refreshed"] += 1
415
+ dirty = True
416
+ except Exception:
417
+ self._record_failure(str(email), retry_after_seconds=None)
418
+ summary["failed"] += 1
419
+ dirty = True
420
+ if dirty:
421
+ data["accountState"] = {
422
+ "failures": self._failures,
423
+ "cooldowns": self._cooldowns,
424
+ "counters": self._counters,
425
+ }
426
+ return dirty
427
+
428
+ update_accounts(mutate)
429
+ return summary
430
+
431
+ def clear_failures(self, email: str) -> None:
432
+ with self._lock:
433
+ self._failures.pop(email, None)
434
+ self._cooldowns.pop(email, None)
435
+ self._save_state_to_storage()