From e42b5b0ba1611a12d0c6a493b4ab6cb72ed44ea8 Mon Sep 17 00:00:00 2001 From: luolong47 Date: Sun, 24 May 2026 22:27:57 +0800 Subject: [PATCH] feat: add lite pool tier and align supported models with super pool --- app/control/account/backends/local.py | 2 +- app/control/account/backends/redis.py | 2 +- app/control/account/backends/sql.py | 2 +- app/control/account/models.py | 8 +++++- app/control/account/quota_defaults.py | 36 ++++++++++++++++++--------- app/control/account/scheduler.py | 1 + app/control/model/enums.py | 5 ++-- app/control/model/spec.py | 24 ++++++++++++------ app/dataplane/account/__init__.py | 1 + app/dataplane/shared/enums.py | 6 +++-- app/products/openai/router.py | 2 +- 11 files changed, 60 insertions(+), 29 deletions(-) diff --git a/app/control/account/backends/local.py b/app/control/account/backends/local.py index 85eb3971f..e1f377d1c 100644 --- a/app/control/account/backends/local.py +++ b/app/control/account/backends/local.py @@ -169,7 +169,7 @@ def _upsert_sync( token = AccountRecord.model_validate({"token": item.token, "pool": item.pool}).token except ValueError: continue - pool = item.pool if item.pool in ("basic", "super", "heavy") else "basic" + pool = item.pool if item.pool in ("basic", "lite", "super", "heavy") else "basic" qs = default_quota_set(pool) conn.execute( f""" diff --git a/app/control/account/backends/redis.py b/app/control/account/backends/redis.py index 68c4d4b49..0764203e1 100644 --- a/app/control/account/backends/redis.py +++ b/app/control/account/backends/redis.py @@ -204,7 +204,7 @@ async def upsert_accounts( token = AccountRecord.model_validate({"token": item.token, "pool": item.pool}).token except ValueError: continue - pool = item.pool if item.pool in ("basic", "super", "heavy") else "basic" + pool = item.pool if item.pool in ("basic", "lite", "super", "heavy") else "basic" qs = default_quota_set(pool) ts = now_ms() record = AccountRecord( diff --git a/app/control/account/backends/sql.py b/app/control/account/backends/sql.py index 66b605bb6..6830069ae 100644 --- a/app/control/account/backends/sql.py +++ b/app/control/account/backends/sql.py @@ -627,7 +627,7 @@ async def upsert_accounts( token = AccountRecord.model_validate({"token": item.token, "pool": item.pool}).token except Exception: continue - pool = item.pool if item.pool in ("basic", "super", "heavy") else "basic" + pool = item.pool if item.pool in ("basic", "lite", "super", "heavy") else "basic" qs = default_quota_set(pool) row = { "token": token, diff --git a/app/control/account/models.py b/app/control/account/models.py index cb8e8e17f..e53f90b1c 100644 --- a/app/control/account/models.py +++ b/app/control/account/models.py @@ -204,6 +204,10 @@ class AccountRecord(BaseModel): def is_nsfw(self) -> bool: return "nsfw" in self.tags + @property + def is_lite(self) -> bool: + return self.pool == "lite" + @property def is_super(self) -> bool: return self.pool == "super" @@ -263,7 +267,9 @@ def _normalize_token(cls, v: Any) -> str: @classmethod def _normalize_pool(cls, v: Any) -> str: val = str(v or "").strip().lower() - if val in ("super"): + if val in ("lite",): + return "lite" + if val in ("super",): return "super" if val in ("heavy",): return "heavy" diff --git a/app/control/account/quota_defaults.py b/app/control/account/quota_defaults.py index eb3250c8d..1e7647635 100644 --- a/app/control/account/quota_defaults.py +++ b/app/control/account/quota_defaults.py @@ -49,6 +49,13 @@ def _w(remaining: int, total: int, window_seconds: int) -> QuotaWindow: expert=_w(0, 0, 0), # unsupported on basic accounts ) +LITE_QUOTA_DEFAULTS = AccountQuotaSet( + auto=_w(25, 25, 7_200), # 25 queries / 2 h + fast=_w(70, 70, 7_200), # 70 queries / 2 h + expert=_w(12, 12, 7_200), # 12 queries / 2 h + grok_4_3=_w(12, 12, 7_200), # 12 queries / 2 h +) + SUPER_QUOTA_DEFAULTS = AccountQuotaSet( auto=_w(50, 50, 7_200), # 50 queries / 2 h fast=_w(140, 140, 7_200), # 140 queries / 2 h @@ -67,26 +74,18 @@ def _w(remaining: int, total: int, window_seconds: int) -> QuotaWindow: # Map pool name → defaults object (used by backends on upsert). _POOL_DEFAULTS: dict[str, AccountQuotaSet] = { "basic": BASIC_QUOTA_DEFAULTS, + "lite": LITE_QUOTA_DEFAULTS, "super": SUPER_QUOTA_DEFAULTS, "heavy": HEAVY_QUOTA_DEFAULTS, } _SUPPORTED_MODE_IDS_BY_POOL: dict[str, frozenset[int]] = { "basic": frozenset((1,)), + "lite": frozenset((0, 1, 2, 4)), "super": frozenset((0, 1, 2, 4)), "heavy": frozenset((0, 1, 2, 3, 4)), } -# --------------------------------------------------------------------------- -# Pool inference — keyed on auto.total (unique across pool types) -# --------------------------------------------------------------------------- - -_AUTO_TOTAL_TO_POOL: dict[int, str] = { - 20: "basic", - 50: "super", - 150: "heavy", -} - def default_quota_set(pool: str) -> AccountQuotaSet: """Return a fresh copy of the default quota set for *pool*.""" @@ -162,17 +161,30 @@ def normalize_quota_set(pool: str, quota_set: AccountQuotaSet) -> AccountQuotaSe def infer_pool(windows: dict[int, QuotaWindow]) -> str: """Infer pool type from live quota windows returned by the rate-limits API. - Uses ``auto.total`` (mode_id=0) as the discriminating signal. + Uses ``auto.total`` (mode_id=0) as the discriminating signal: + - 25 -> lite + - 50 -> super + - 150 -> heavy + - otherwise -> basic Falls back to ``"basic"`` when the value is absent or unrecognised. """ auto_win = windows.get(0) if auto_win is None: return "basic" - return _AUTO_TOTAL_TO_POOL.get(auto_win.total, "basic") + total = auto_win.total + if total == 25: + return "lite" + elif total == 50: + return "super" + elif total >= 150: + return "heavy" + else: + return "basic" __all__ = [ "BASIC_QUOTA_DEFAULTS", + "LITE_QUOTA_DEFAULTS", "SUPER_QUOTA_DEFAULTS", "HEAVY_QUOTA_DEFAULTS", "default_quota_set", diff --git a/app/control/account/scheduler.py b/app/control/account/scheduler.py index e141bb987..50fb447a9 100644 --- a/app/control/account/scheduler.py +++ b/app/control/account/scheduler.py @@ -17,6 +17,7 @@ # Pool → (config key, built-in default seconds) _POOL_CONFIG: dict[str, tuple[str, int]] = { "basic": ("account.refresh.basic_interval_sec", 86_400), + "lite": ("account.refresh.super_interval_sec", 7_200), "super": ("account.refresh.super_interval_sec", 7_200), "heavy": ("account.refresh.heavy_interval_sec", 7_200), } diff --git a/app/control/model/enums.py b/app/control/model/enums.py index d2de5c93b..651bf461b 100644 --- a/app/control/model/enums.py +++ b/app/control/model/enums.py @@ -26,8 +26,9 @@ class Tier(IntEnum): """Account tier — determines which pool is selected.""" BASIC = 0 # pool="basic" - SUPER = 1 # pool="super" - HEAVY = 2 # pool="heavy" + LITE = 1 # pool="lite" + SUPER = 2 # pool="super" + HEAVY = 3 # pool="heavy" class Capability(IntFlag): diff --git a/app/control/model/spec.py b/app/control/model/spec.py index c860be0f5..37553d4f3 100644 --- a/app/control/model/spec.py +++ b/app/control/model/spec.py @@ -49,6 +49,8 @@ def is_voice(self) -> bool: def pool_name(self) -> str: """Return the canonical pool string for this tier.""" + if self.tier == Tier.LITE: + return "lite" if self.tier == Tier.SUPER: return "super" if self.tier == Tier.HEAVY: @@ -69,26 +71,32 @@ def pool_candidates(self) -> tuple[int, ...]: when all accounts in higher pools are exhausted). Default (prefer_best=False): - BASIC tier → try basic first, then super, then heavy + BASIC tier → try basic first, then lite, then super, then heavy + LITE tier → try lite first, then super, then heavy SUPER tier → try super first, then heavy HEAVY tier → heavy only Reversed (prefer_best=True): - BASIC tier → try heavy first, then super, then basic + BASIC tier → try heavy first, then super, then lite, then basic + LITE tier → try heavy first, then super, then lite SUPER tier → try heavy first, then super HEAVY tier → heavy only """ if self.prefer_best: if self.tier == Tier.HEAVY: - return (2,) # heavy only + return (3,) # heavy only if self.tier == Tier.SUPER: - return (2, 1) # heavy, super - return (2, 1, 0) # heavy, super, basic + return (3, 2, 1) # heavy, super, lite + if self.tier == Tier.LITE: + return (3, 2, 1) # heavy, super, lite + return (3, 2, 1, 0) # heavy, super, lite, basic if self.tier == Tier.BASIC: - return (0, 1, 2) # basic, super, heavy + return (0, 1, 2, 3) # basic, lite, super, heavy + if self.tier == Tier.LITE: + return (1, 2, 3) # lite, super, heavy if self.tier == Tier.SUPER: - return (1, 2) # super, heavy - return (2,) # heavy only + return (2, 1, 3) # super, lite, heavy + return (3,) # heavy only __all__ = ["ModelSpec"] diff --git a/app/dataplane/account/__init__.py b/app/dataplane/account/__init__.py index c3c2f62e5..e70b80114 100644 --- a/app/dataplane/account/__init__.py +++ b/app/dataplane/account/__init__.py @@ -308,6 +308,7 @@ def revision(self) -> int: _POOL_INTERVAL_CONFIG: dict[str, tuple[str, int]] = { "basic": ("account.refresh.basic_interval_sec", 86_400), + "lite": ("account.refresh.super_interval_sec", 7_200), "super": ("account.refresh.super_interval_sec", 7_200), "heavy": ("account.refresh.heavy_interval_sec", 7_200), } diff --git a/app/dataplane/shared/enums.py b/app/dataplane/shared/enums.py index 88b9ed66b..dcb2ace71 100644 --- a/app/dataplane/shared/enums.py +++ b/app/dataplane/shared/enums.py @@ -17,8 +17,9 @@ class ModeId(IntEnum): class PoolId(IntEnum): BASIC = 0 - SUPER = 1 - HEAVY = 2 + LITE = 1 + SUPER = 2 + HEAVY = 3 class StatusId(IntEnum): @@ -32,6 +33,7 @@ class StatusId(IntEnum): # Map pool string → PoolId integer (used during sync from control plane). POOL_STR_TO_ID: dict[str, int] = { "basic": int(PoolId.BASIC), + "lite": int(PoolId.LITE), "super": int(PoolId.SUPER), "heavy": int(PoolId.HEAVY), } diff --git a/app/products/openai/router.py b/app/products/openai/router.py index 01a27504a..fddd6cdcf 100644 --- a/app/products/openai/router.py +++ b/app/products/openai/router.py @@ -27,7 +27,7 @@ from .chat import completions as chat_completions router = APIRouter(prefix="/v1") -_POOL_ID_TO_NAME = {0: "basic", 1: "super", 2: "heavy"} +_POOL_ID_TO_NAME = {0: "basic", 1: "lite", 2: "super", 3: "heavy"} _TAG_MODELS = "OpenAI - Models" _TAG_CHAT = "OpenAI - Chat" _TAG_RESPONSES = "OpenAI - Responses"