افزودن ارائه‌دهندهٔ agy (Google Antigravity) با لاگین headless و پول چرخشی اکانت

- Adapter اکانت‌محور با چرخش round-robin و بنچ‌شدن خودکار روی ۴۲۹/۴۰۱
- ورود ادمین از طریق URL بازگشتی گوگل (PKCE) و ذخیرهٔ رمزنگاری‌شدهٔ refresh token
- صفحهٔ مدیریت اکانت‌های agy در پنل ادمین به همراه تست و فعال/غیرفعال‌سازی
- مهاجرت Alembic b1c2d3e4f5a6 برای ستون‌های اکانت agy
- seed همیشه کاتالوگ agy را می‌سازد و فقط با AI_DEFAULT_PROVIDER=agy مسیریابی می‌شود
This commit is contained in:
mjalmoudy94
2026-10-13 08:20:27 +03:30
parent 54074f10c3
commit 3d1e98f4fb
25 changed files with 3073 additions and 10 deletions
+30
View File
@@ -19,6 +19,19 @@ class ProviderType(StrEnum):
ANTHROPIC = "anthropic"
GEMINI = "gemini"
OPENROUTER = "openrouter"
#: Google Antigravity (``agy``) — OAuth accounts billed through a Google AI
#: subscription. Credentials of this type are *accounts*, not API keys.
AGY = "agy"
class CredentialStatus(StrEnum):
"""Lifecycle of one agy account in the rotation pool."""
ACTIVE = "active"
#: Rate limited by Google; skipped until ``cooldown_until``.
COOLDOWN = "cooldown"
#: Refresh token rejected or another hard error; needs re-login.
ERROR = "error"
class RoutingMode(StrEnum):
@@ -61,12 +74,29 @@ class Provider:
@dataclass(slots=True, kw_only=True)
class ModelCredential:
"""One credential for a provider.
For key-based providers ``encrypted_api_key`` holds the encrypted API key and
the agy fields stay empty. For the agy provider the credential *is an
account*: ``encrypted_api_key`` holds the encrypted JSON blob
``{"refresh_token": ..., "project_id": ..., "email": ...}`` while ``email`` /
``project_id`` duplicate the identity columns so the admin console can show
and rotate accounts without touching the ciphertext.
"""
provider_id: str
name: str
encrypted_api_key: str
id: str = field(default_factory=new_id)
is_active: bool = True
created_at: datetime = field(default_factory=utcnow)
#: agy account identity (empty for API-key credentials).
email: str = ""
project_id: str = ""
status: str = CredentialStatus.ACTIVE.value
cooldown_until: datetime | None = None
last_error: str = ""
last_used_at: datetime | None = None
@dataclass(slots=True, kw_only=True)