mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
refactor(copilot): rename RateLimitTier to SubscriptionTier with Prisma enum
Rename `rateLimitTier` (String) to `subscriptionTier` (Prisma enum) across the entire stack: - schema.prisma: Add `SubscriptionTier` enum (FREE, STANDARD, PRO, ENTERPRISE), change User field from `rateLimitTier String` to `subscriptionTier SubscriptionTier`. - migration.sql: CREATE TYPE + ALTER TABLE for the new enum column. - rate_limit.py: Rename Python enum and update DB field references. - All test files, admin routes, snapshots, and openapi.json updated to match the new naming. Addresses PR feedback asking for a generic name and proper Prisma enum instead of a free-form string.
This commit is contained in:
@@ -9,7 +9,7 @@ from pydantic import BaseModel
|
||||
|
||||
from backend.copilot.config import ChatConfig
|
||||
from backend.copilot.rate_limit import (
|
||||
RateLimitTier,
|
||||
SubscriptionTier,
|
||||
get_global_rate_limits,
|
||||
get_usage_status,
|
||||
get_user_tier,
|
||||
@@ -34,17 +34,17 @@ class UserRateLimitResponse(BaseModel):
|
||||
weekly_token_limit: int
|
||||
daily_tokens_used: int
|
||||
weekly_tokens_used: int
|
||||
tier: RateLimitTier
|
||||
tier: SubscriptionTier
|
||||
|
||||
|
||||
class UserTierResponse(BaseModel):
|
||||
user_id: str
|
||||
tier: RateLimitTier
|
||||
tier: SubscriptionTier
|
||||
|
||||
|
||||
class SetUserTierRequest(BaseModel):
|
||||
user_id: str
|
||||
tier: RateLimitTier
|
||||
tier: SubscriptionTier
|
||||
|
||||
|
||||
@router.get(
|
||||
|
||||
@@ -8,7 +8,7 @@ import pytest_mock
|
||||
from autogpt_libs.auth.jwt_utils import get_jwt_payload
|
||||
from pytest_snapshot.plugin import Snapshot
|
||||
|
||||
from backend.copilot.rate_limit import CoPilotUsageStatus, RateLimitTier, UsageWindow
|
||||
from backend.copilot.rate_limit import CoPilotUsageStatus, SubscriptionTier, UsageWindow
|
||||
|
||||
from .rate_limit_admin_routes import router as rate_limit_admin_router
|
||||
|
||||
@@ -53,7 +53,7 @@ def test_get_rate_limit(
|
||||
mocker.patch(
|
||||
f"{_MOCK_MODULE}.get_global_rate_limits",
|
||||
new_callable=AsyncMock,
|
||||
return_value=(2_500_000, 12_500_000, RateLimitTier.STANDARD),
|
||||
return_value=(2_500_000, 12_500_000, SubscriptionTier.FREE),
|
||||
)
|
||||
mocker.patch(
|
||||
f"{_MOCK_MODULE}.get_usage_status",
|
||||
@@ -70,7 +70,7 @@ def test_get_rate_limit(
|
||||
assert data["weekly_token_limit"] == 12_500_000
|
||||
assert data["daily_tokens_used"] == 500_000
|
||||
assert data["weekly_tokens_used"] == 3_000_000
|
||||
assert data["tier"] == "standard"
|
||||
assert data["tier"] == "FREE"
|
||||
|
||||
configured_snapshot.assert_match(
|
||||
json.dumps(data, indent=2, sort_keys=True) + "\n",
|
||||
@@ -91,7 +91,7 @@ def test_reset_user_usage_daily_only(
|
||||
mocker.patch(
|
||||
f"{_MOCK_MODULE}.get_global_rate_limits",
|
||||
new_callable=AsyncMock,
|
||||
return_value=(2_500_000, 12_500_000, RateLimitTier.STANDARD),
|
||||
return_value=(2_500_000, 12_500_000, SubscriptionTier.FREE),
|
||||
)
|
||||
mocker.patch(
|
||||
f"{_MOCK_MODULE}.get_usage_status",
|
||||
@@ -109,7 +109,7 @@ def test_reset_user_usage_daily_only(
|
||||
assert data["daily_tokens_used"] == 0
|
||||
# Weekly is untouched
|
||||
assert data["weekly_tokens_used"] == 3_000_000
|
||||
assert data["tier"] == "standard"
|
||||
assert data["tier"] == "FREE"
|
||||
|
||||
mock_reset.assert_awaited_once_with(target_user_id, reset_weekly=False)
|
||||
|
||||
@@ -132,7 +132,7 @@ def test_reset_user_usage_daily_and_weekly(
|
||||
mocker.patch(
|
||||
f"{_MOCK_MODULE}.get_global_rate_limits",
|
||||
new_callable=AsyncMock,
|
||||
return_value=(2_500_000, 12_500_000, RateLimitTier.STANDARD),
|
||||
return_value=(2_500_000, 12_500_000, SubscriptionTier.FREE),
|
||||
)
|
||||
mocker.patch(
|
||||
f"{_MOCK_MODULE}.get_usage_status",
|
||||
@@ -149,7 +149,7 @@ def test_reset_user_usage_daily_and_weekly(
|
||||
data = response.json()
|
||||
assert data["daily_tokens_used"] == 0
|
||||
assert data["weekly_tokens_used"] == 0
|
||||
assert data["tier"] == "standard"
|
||||
assert data["tier"] == "FREE"
|
||||
|
||||
mock_reset.assert_awaited_once_with(target_user_id, reset_weekly=True)
|
||||
|
||||
@@ -205,7 +205,7 @@ def test_get_user_tier(
|
||||
mocker.patch(
|
||||
f"{_MOCK_MODULE}.get_user_tier",
|
||||
new_callable=AsyncMock,
|
||||
return_value=RateLimitTier.PRO,
|
||||
return_value=SubscriptionTier.PRO,
|
||||
)
|
||||
|
||||
response = client.get("/admin/rate_limit/tier", params={"user_id": target_user_id})
|
||||
@@ -213,7 +213,7 @@ def test_get_user_tier(
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["user_id"] == target_user_id
|
||||
assert data["tier"] == "pro"
|
||||
assert data["tier"] == "PRO"
|
||||
|
||||
|
||||
def test_set_user_tier(
|
||||
@@ -228,14 +228,14 @@ def test_set_user_tier(
|
||||
|
||||
response = client.post(
|
||||
"/admin/rate_limit/tier",
|
||||
json={"user_id": target_user_id, "tier": "max"},
|
||||
json={"user_id": target_user_id, "tier": "ENTERPRISE"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["user_id"] == target_user_id
|
||||
assert data["tier"] == "max"
|
||||
mock_set.assert_awaited_once_with(target_user_id, RateLimitTier.MAX)
|
||||
assert data["tier"] == "ENTERPRISE"
|
||||
mock_set.assert_awaited_once_with(target_user_id, SubscriptionTier.ENTERPRISE)
|
||||
|
||||
|
||||
def test_set_user_tier_invalid_tier(
|
||||
@@ -265,7 +265,7 @@ def test_set_user_tier_user_not_found(
|
||||
|
||||
response = client.post(
|
||||
"/admin/rate_limit/tier",
|
||||
json={"user_id": target_user_id, "tier": "pro"},
|
||||
json={"user_id": target_user_id, "tier": "PRO"},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
@@ -284,7 +284,7 @@ def test_set_user_tier_db_failure(
|
||||
|
||||
response = client.post(
|
||||
"/admin/rate_limit/tier",
|
||||
json={"user_id": target_user_id, "tier": "pro"},
|
||||
json={"user_id": target_user_id, "tier": "PRO"},
|
||||
)
|
||||
|
||||
assert response.status_code == 500
|
||||
@@ -299,6 +299,6 @@ def test_tier_endpoints_require_admin_role(mock_jwt_user) -> None:
|
||||
|
||||
response = client.post(
|
||||
"/admin/rate_limit/tier",
|
||||
json={"user_id": "test", "tier": "pro"},
|
||||
json={"user_id": "test", "tier": "PRO"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
import pytest_mock
|
||||
|
||||
from backend.api.features.chat import routes as chat_routes
|
||||
from backend.copilot.rate_limit import RateLimitTier
|
||||
from backend.copilot.rate_limit import SubscriptionTier
|
||||
|
||||
app = fastapi.FastAPI()
|
||||
app.include_router(chat_routes.router)
|
||||
@@ -370,7 +370,7 @@ def test_usage_returns_daily_and_weekly(
|
||||
daily_token_limit=10000,
|
||||
weekly_token_limit=50000,
|
||||
rate_limit_reset_cost=chat_routes.config.rate_limit_reset_cost,
|
||||
tier=RateLimitTier.STANDARD,
|
||||
tier=SubscriptionTier.FREE,
|
||||
)
|
||||
|
||||
|
||||
@@ -393,7 +393,7 @@ def test_usage_uses_config_limits(
|
||||
daily_token_limit=99999,
|
||||
weekly_token_limit=77777,
|
||||
rate_limit_reset_cost=500,
|
||||
tier=RateLimitTier.STANDARD,
|
||||
tier=SubscriptionTier.FREE,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ async def test_create_store_submission(mocker):
|
||||
notifyOnAgentApproved=True,
|
||||
notifyOnAgentRejected=True,
|
||||
timezone="Europe/Delft",
|
||||
rateLimitTier="standard",
|
||||
subscriptionTier=prisma.enums.SubscriptionTier.FREE,
|
||||
)
|
||||
mock_agent = prisma.models.AgentGraph(
|
||||
id="agent-id",
|
||||
|
||||
@@ -77,10 +77,11 @@ class ChatConfig(BaseSettings):
|
||||
# allows ~70-100 turns/day.
|
||||
# Checked at the HTTP layer (routes.py) before each turn.
|
||||
#
|
||||
# These are base limits for the "standard" tier. Higher tiers (pro, max)
|
||||
# multiply these by their tier multiplier (see rate_limit.TIER_MULTIPLIERS).
|
||||
# User tier is stored in the User.rateLimitTier DB column and resolved
|
||||
# inside get_global_rate_limits().
|
||||
# These are base limits for the FREE tier. Higher tiers (STANDARD, PRO,
|
||||
# ENTERPRISE) multiply these by their tier multiplier (see
|
||||
# rate_limit.TIER_MULTIPLIERS). User tier is stored in the
|
||||
# User.subscriptionTier DB column and resolved inside
|
||||
# get_global_rate_limits().
|
||||
daily_token_limit: int = Field(
|
||||
default=2_500_000,
|
||||
description="Max tokens per day, resets at midnight UTC (0 = unlimited)",
|
||||
|
||||
@@ -25,26 +25,28 @@ _USAGE_KEY_PREFIX = "copilot:usage"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rate-limit tier definitions
|
||||
# Subscription tier definitions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class RateLimitTier(str, Enum):
|
||||
"""Rate-limit tiers with increasing token allowances."""
|
||||
class SubscriptionTier(str, Enum):
|
||||
"""Subscription tiers with increasing token allowances."""
|
||||
|
||||
STANDARD = "standard"
|
||||
PRO = "pro"
|
||||
MAX = "max"
|
||||
FREE = "FREE"
|
||||
STANDARD = "STANDARD"
|
||||
PRO = "PRO"
|
||||
ENTERPRISE = "ENTERPRISE"
|
||||
|
||||
|
||||
# Multiplier applied to the base limits (from LD / config) for each tier.
|
||||
TIER_MULTIPLIERS: dict[RateLimitTier, int] = {
|
||||
RateLimitTier.STANDARD: 1,
|
||||
RateLimitTier.PRO: 5,
|
||||
RateLimitTier.MAX: 25,
|
||||
TIER_MULTIPLIERS: dict[SubscriptionTier, int] = {
|
||||
SubscriptionTier.FREE: 1,
|
||||
SubscriptionTier.STANDARD: 5,
|
||||
SubscriptionTier.PRO: 10,
|
||||
SubscriptionTier.ENTERPRISE: 25,
|
||||
}
|
||||
|
||||
DEFAULT_TIER = RateLimitTier.STANDARD
|
||||
DEFAULT_TIER = SubscriptionTier.FREE
|
||||
|
||||
|
||||
class UsageWindow(BaseModel):
|
||||
@@ -62,7 +64,7 @@ class CoPilotUsageStatus(BaseModel):
|
||||
|
||||
daily: UsageWindow
|
||||
weekly: UsageWindow
|
||||
tier: RateLimitTier = DEFAULT_TIER
|
||||
tier: SubscriptionTier = DEFAULT_TIER
|
||||
reset_cost: int = Field(
|
||||
default=0,
|
||||
description="Credit cost (in cents) to reset the daily limit. 0 = feature disabled.",
|
||||
@@ -93,7 +95,7 @@ async def get_usage_status(
|
||||
daily_token_limit: int,
|
||||
weekly_token_limit: int,
|
||||
rate_limit_reset_cost: int = 0,
|
||||
tier: RateLimitTier = DEFAULT_TIER,
|
||||
tier: SubscriptionTier = DEFAULT_TIER,
|
||||
) -> CoPilotUsageStatus:
|
||||
"""Get current usage status for a user.
|
||||
|
||||
@@ -373,19 +375,19 @@ async def record_token_usage(
|
||||
|
||||
|
||||
@cached(maxsize=1000, ttl_seconds=300)
|
||||
async def _fetch_user_tier(user_id: str) -> RateLimitTier:
|
||||
async def _fetch_user_tier(user_id: str) -> SubscriptionTier:
|
||||
"""Fetch the user's rate-limit tier from the database (cached).
|
||||
|
||||
Only successful DB lookups are cached. Raises on DB errors so the
|
||||
``@cached`` decorator does **not** store a fallback value.
|
||||
"""
|
||||
user = await PrismaUser.prisma().find_unique(where={"id": user_id})
|
||||
if user and user.rateLimitTier:
|
||||
return RateLimitTier(user.rateLimitTier)
|
||||
if user and user.subscriptionTier:
|
||||
return SubscriptionTier(user.subscriptionTier)
|
||||
return DEFAULT_TIER
|
||||
|
||||
|
||||
async def get_user_tier(user_id: str) -> RateLimitTier:
|
||||
async def get_user_tier(user_id: str) -> SubscriptionTier:
|
||||
"""Look up the user's rate-limit tier from the database.
|
||||
|
||||
Successful results are cached for 5 minutes (via ``_fetch_user_tier``)
|
||||
@@ -413,7 +415,7 @@ get_user_tier.cache_clear = _fetch_user_tier.cache_clear # type: ignore[attr-de
|
||||
get_user_tier.cache_delete = _fetch_user_tier.cache_delete # type: ignore[attr-defined]
|
||||
|
||||
|
||||
async def set_user_tier(user_id: str, tier: RateLimitTier) -> None:
|
||||
async def set_user_tier(user_id: str, tier: SubscriptionTier) -> None:
|
||||
"""Persist the user's rate-limit tier to the database.
|
||||
|
||||
Also invalidates the ``get_user_tier`` cache for this user so that
|
||||
@@ -424,7 +426,7 @@ async def set_user_tier(user_id: str, tier: RateLimitTier) -> None:
|
||||
"""
|
||||
await PrismaUser.prisma().update(
|
||||
where={"id": user_id},
|
||||
data={"rateLimitTier": tier.value},
|
||||
data={"subscriptionTier": tier.value},
|
||||
)
|
||||
# Invalidate cached tier so rate-limit checks pick up the change immediately.
|
||||
get_user_tier.cache_delete(user_id) # type: ignore[attr-defined]
|
||||
@@ -434,7 +436,7 @@ async def get_global_rate_limits(
|
||||
user_id: str,
|
||||
config_daily: int,
|
||||
config_weekly: int,
|
||||
) -> tuple[int, int, RateLimitTier]:
|
||||
) -> tuple[int, int, SubscriptionTier]:
|
||||
"""Resolve global rate limits from LaunchDarkly, falling back to config.
|
||||
|
||||
The base limits (from LD or config) are multiplied by the user's
|
||||
|
||||
@@ -11,7 +11,7 @@ from .rate_limit import (
|
||||
TIER_MULTIPLIERS,
|
||||
CoPilotUsageStatus,
|
||||
RateLimitExceeded,
|
||||
RateLimitTier,
|
||||
SubscriptionTier,
|
||||
UsageWindow,
|
||||
check_rate_limit,
|
||||
get_global_rate_limits,
|
||||
@@ -342,23 +342,25 @@ class TestRecordTokenUsage:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RateLimitTier and tier multipliers
|
||||
# SubscriptionTier and tier multipliers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRateLimitTier:
|
||||
class TestSubscriptionTier:
|
||||
def test_tier_values(self):
|
||||
assert RateLimitTier.STANDARD.value == "standard"
|
||||
assert RateLimitTier.PRO.value == "pro"
|
||||
assert RateLimitTier.MAX.value == "max"
|
||||
assert SubscriptionTier.FREE.value == "FREE"
|
||||
assert SubscriptionTier.STANDARD.value == "STANDARD"
|
||||
assert SubscriptionTier.PRO.value == "PRO"
|
||||
assert SubscriptionTier.ENTERPRISE.value == "ENTERPRISE"
|
||||
|
||||
def test_tier_multipliers(self):
|
||||
assert TIER_MULTIPLIERS[RateLimitTier.STANDARD] == 1
|
||||
assert TIER_MULTIPLIERS[RateLimitTier.PRO] == 5
|
||||
assert TIER_MULTIPLIERS[RateLimitTier.MAX] == 25
|
||||
assert TIER_MULTIPLIERS[SubscriptionTier.FREE] == 1
|
||||
assert TIER_MULTIPLIERS[SubscriptionTier.STANDARD] == 5
|
||||
assert TIER_MULTIPLIERS[SubscriptionTier.PRO] == 10
|
||||
assert TIER_MULTIPLIERS[SubscriptionTier.ENTERPRISE] == 25
|
||||
|
||||
def test_default_tier_is_standard(self):
|
||||
assert DEFAULT_TIER == RateLimitTier.STANDARD
|
||||
def test_default_tier_is_free(self):
|
||||
assert DEFAULT_TIER == SubscriptionTier.FREE
|
||||
|
||||
def test_usage_status_includes_tier(self):
|
||||
now = datetime.now(UTC)
|
||||
@@ -366,17 +368,17 @@ class TestRateLimitTier:
|
||||
daily=UsageWindow(used=0, limit=100, resets_at=now + timedelta(hours=1)),
|
||||
weekly=UsageWindow(used=0, limit=500, resets_at=now + timedelta(days=1)),
|
||||
)
|
||||
# Default tier should be STANDARD
|
||||
assert status.tier == RateLimitTier.STANDARD
|
||||
# Default tier should be FREE
|
||||
assert status.tier == SubscriptionTier.FREE
|
||||
|
||||
def test_usage_status_with_custom_tier(self):
|
||||
now = datetime.now(UTC)
|
||||
status = CoPilotUsageStatus(
|
||||
daily=UsageWindow(used=0, limit=100, resets_at=now + timedelta(hours=1)),
|
||||
weekly=UsageWindow(used=0, limit=500, resets_at=now + timedelta(days=1)),
|
||||
tier=RateLimitTier.PRO,
|
||||
tier=SubscriptionTier.PRO,
|
||||
)
|
||||
assert status.tier == RateLimitTier.PRO
|
||||
assert status.tier == SubscriptionTier.PRO
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -394,7 +396,7 @@ class TestGetUserTier:
|
||||
async def test_returns_tier_from_db(self):
|
||||
"""Should return the tier stored in the user record."""
|
||||
mock_user = MagicMock()
|
||||
mock_user.rateLimitTier = "pro"
|
||||
mock_user.subscriptionTier = "PRO"
|
||||
|
||||
mock_prisma = AsyncMock()
|
||||
mock_prisma.find_unique = AsyncMock(return_value=mock_user)
|
||||
@@ -405,7 +407,7 @@ class TestGetUserTier:
|
||||
):
|
||||
tier = await get_user_tier(_USER)
|
||||
|
||||
assert tier == RateLimitTier.PRO
|
||||
assert tier == SubscriptionTier.PRO
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_default_when_user_not_found(self):
|
||||
@@ -423,9 +425,9 @@ class TestGetUserTier:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_default_when_tier_is_none(self):
|
||||
"""Should return DEFAULT_TIER when rateLimitTier is None."""
|
||||
"""Should return DEFAULT_TIER when subscriptionTier is None."""
|
||||
mock_user = MagicMock()
|
||||
mock_user.rateLimitTier = None
|
||||
mock_user.subscriptionTier = None
|
||||
|
||||
mock_prisma = AsyncMock()
|
||||
mock_prisma.find_unique = AsyncMock(return_value=mock_user)
|
||||
@@ -471,7 +473,7 @@ class TestGetUserTier:
|
||||
|
||||
# Now DB recovers and returns PRO
|
||||
mock_user = MagicMock()
|
||||
mock_user.rateLimitTier = "pro"
|
||||
mock_user.subscriptionTier = "PRO"
|
||||
ok_prisma = AsyncMock()
|
||||
ok_prisma.find_unique = AsyncMock(return_value=mock_user)
|
||||
|
||||
@@ -482,13 +484,13 @@ class TestGetUserTier:
|
||||
tier2 = await get_user_tier(_USER)
|
||||
|
||||
# Should get PRO now — the error result was not cached
|
||||
assert tier2 == RateLimitTier.PRO
|
||||
assert tier2 == SubscriptionTier.PRO
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_default_on_invalid_tier_value(self):
|
||||
"""Should fall back to DEFAULT_TIER when stored value is invalid."""
|
||||
mock_user = MagicMock()
|
||||
mock_user.rateLimitTier = "invalid-tier"
|
||||
mock_user.subscriptionTier = "invalid-tier"
|
||||
|
||||
mock_prisma = AsyncMock()
|
||||
mock_prisma.find_unique = AsyncMock(return_value=mock_user)
|
||||
@@ -523,13 +525,13 @@ class TestGetGlobalRateLimitsWithTiers:
|
||||
return _side_effect
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_standard_tier_no_multiplier(self):
|
||||
"""Standard tier should not change limits."""
|
||||
async def test_free_tier_no_multiplier(self):
|
||||
"""Free tier should not change limits."""
|
||||
with (
|
||||
patch(
|
||||
"backend.copilot.rate_limit.get_user_tier",
|
||||
new_callable=AsyncMock,
|
||||
return_value=RateLimitTier.STANDARD,
|
||||
return_value=SubscriptionTier.FREE,
|
||||
),
|
||||
patch(
|
||||
"backend.util.feature_flag.get_feature_flag_value",
|
||||
@@ -542,16 +544,16 @@ class TestGetGlobalRateLimitsWithTiers:
|
||||
|
||||
assert daily == 2_500_000
|
||||
assert weekly == 12_500_000
|
||||
assert tier == RateLimitTier.STANDARD
|
||||
assert tier == SubscriptionTier.FREE
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pro_tier_5x_multiplier(self):
|
||||
"""Pro tier should multiply limits by 5."""
|
||||
async def test_standard_tier_5x_multiplier(self):
|
||||
"""Standard tier should multiply limits by 5."""
|
||||
with (
|
||||
patch(
|
||||
"backend.copilot.rate_limit.get_user_tier",
|
||||
new_callable=AsyncMock,
|
||||
return_value=RateLimitTier.PRO,
|
||||
return_value=SubscriptionTier.STANDARD,
|
||||
),
|
||||
patch(
|
||||
"backend.util.feature_flag.get_feature_flag_value",
|
||||
@@ -564,16 +566,16 @@ class TestGetGlobalRateLimitsWithTiers:
|
||||
|
||||
assert daily == 12_500_000
|
||||
assert weekly == 62_500_000
|
||||
assert tier == RateLimitTier.PRO
|
||||
assert tier == SubscriptionTier.STANDARD
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_tier_25x_multiplier(self):
|
||||
"""Max tier should multiply limits by 25."""
|
||||
async def test_enterprise_tier_25x_multiplier(self):
|
||||
"""Enterprise tier should multiply limits by 25."""
|
||||
with (
|
||||
patch(
|
||||
"backend.copilot.rate_limit.get_user_tier",
|
||||
new_callable=AsyncMock,
|
||||
return_value=RateLimitTier.MAX,
|
||||
return_value=SubscriptionTier.ENTERPRISE,
|
||||
),
|
||||
patch(
|
||||
"backend.util.feature_flag.get_feature_flag_value",
|
||||
@@ -586,7 +588,7 @@ class TestGetGlobalRateLimitsWithTiers:
|
||||
|
||||
assert daily == 62_500_000
|
||||
assert weekly == 312_500_000
|
||||
assert tier == RateLimitTier.MAX
|
||||
assert tier == SubscriptionTier.ENTERPRISE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1841,7 +1841,7 @@ async def stream_chat_completion_sdk(
|
||||
"conversation_turn": str(turn),
|
||||
}
|
||||
if _user_tier:
|
||||
_otel_metadata["rate_limit_tier"] = _user_tier.value
|
||||
_otel_metadata["subscription_tier"] = _user_tier.value
|
||||
|
||||
_otel_ctx = propagate_attributes(
|
||||
user_id=user_id,
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubscriptionTier" AS ENUM ('FREE', 'STANDARD', 'PRO', 'ENTERPRISE');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "rateLimitTier" TEXT NOT NULL DEFAULT 'standard';
|
||||
ALTER TABLE "User" ADD COLUMN "subscriptionTier" "SubscriptionTier" NOT NULL DEFAULT 'FREE';
|
||||
|
||||
@@ -40,9 +40,9 @@ model User {
|
||||
|
||||
timezone String @default("not-set")
|
||||
|
||||
// CoPilot rate-limit tier: "standard" (default), "pro", or "max".
|
||||
// Multipliers applied in get_global_rate_limits(): standard=1x, pro=5x, max=25x.
|
||||
rateLimitTier String @default("standard")
|
||||
// CoPilot subscription tier — controls rate-limit multipliers.
|
||||
// Multipliers applied in get_global_rate_limits(): FREE=1x, STANDARD=5x, PRO=10x, ENTERPRISE=25x.
|
||||
subscriptionTier SubscriptionTier @default(FREE)
|
||||
|
||||
// Relations
|
||||
|
||||
@@ -77,6 +77,13 @@ model User {
|
||||
OAuthRefreshTokens OAuthRefreshToken[]
|
||||
}
|
||||
|
||||
enum SubscriptionTier {
|
||||
FREE
|
||||
STANDARD
|
||||
PRO
|
||||
ENTERPRISE
|
||||
}
|
||||
|
||||
enum OnboardingStep {
|
||||
// Introductory onboarding (Library)
|
||||
WELCOME
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"daily_token_limit": 2500000,
|
||||
"daily_tokens_used": 500000,
|
||||
"tier": "standard",
|
||||
"tier": "FREE",
|
||||
"user_id": "5e53486c-cf57-477e-ba2a-cb02dc828e1c",
|
||||
"weekly_token_limit": 12500000,
|
||||
"weekly_tokens_used": 3000000
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"daily_token_limit": 2500000,
|
||||
"daily_tokens_used": 0,
|
||||
"tier": "standard",
|
||||
"tier": "FREE",
|
||||
"user_id": "5e53486c-cf57-477e-ba2a-cb02dc828e1c",
|
||||
"weekly_token_limit": 12500000,
|
||||
"weekly_tokens_used": 0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"daily_token_limit": 2500000,
|
||||
"daily_tokens_used": 0,
|
||||
"tier": "standard",
|
||||
"tier": "FREE",
|
||||
"user_id": "5e53486c-cf57-477e-ba2a-cb02dc828e1c",
|
||||
"weekly_token_limit": 12500000,
|
||||
"weekly_tokens_used": 3000000
|
||||
|
||||
@@ -8478,8 +8478,8 @@
|
||||
"daily": { "$ref": "#/components/schemas/UsageWindow" },
|
||||
"weekly": { "$ref": "#/components/schemas/UsageWindow" },
|
||||
"tier": {
|
||||
"$ref": "#/components/schemas/RateLimitTier",
|
||||
"default": "standard"
|
||||
"$ref": "#/components/schemas/SubscriptionTier",
|
||||
"default": "FREE"
|
||||
},
|
||||
"reset_cost": {
|
||||
"type": "integer",
|
||||
@@ -11823,12 +11823,6 @@
|
||||
"title": "RateLimitResetResponse",
|
||||
"description": "Response from resetting the daily rate limit."
|
||||
},
|
||||
"RateLimitTier": {
|
||||
"type": "string",
|
||||
"enum": ["standard", "pro", "max"],
|
||||
"title": "RateLimitTier",
|
||||
"description": "Rate-limit tiers with increasing token allowances."
|
||||
},
|
||||
"RecentExecution": {
|
||||
"properties": {
|
||||
"status": { "type": "string", "title": "Status" },
|
||||
@@ -12218,7 +12212,7 @@
|
||||
"SetUserTierRequest": {
|
||||
"properties": {
|
||||
"user_id": { "type": "string", "title": "User Id" },
|
||||
"tier": { "$ref": "#/components/schemas/RateLimitTier" }
|
||||
"tier": { "$ref": "#/components/schemas/SubscriptionTier" }
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["user_id", "tier"],
|
||||
@@ -12993,6 +12987,12 @@
|
||||
"enum": ["DRAFT", "PENDING", "APPROVED", "REJECTED"],
|
||||
"title": "SubmissionStatus"
|
||||
},
|
||||
"SubscriptionTier": {
|
||||
"type": "string",
|
||||
"enum": ["FREE", "STANDARD", "PRO", "ENTERPRISE"],
|
||||
"title": "SubscriptionTier",
|
||||
"description": "Subscription tiers with increasing token allowances."
|
||||
},
|
||||
"SuggestedGoalResponse": {
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -14808,7 +14808,7 @@
|
||||
"type": "integer",
|
||||
"title": "Weekly Tokens Used"
|
||||
},
|
||||
"tier": { "$ref": "#/components/schemas/RateLimitTier" }
|
||||
"tier": { "$ref": "#/components/schemas/SubscriptionTier" }
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -14847,7 +14847,7 @@
|
||||
"UserTierResponse": {
|
||||
"properties": {
|
||||
"user_id": { "type": "string", "title": "User Id" },
|
||||
"tier": { "$ref": "#/components/schemas/RateLimitTier" }
|
||||
"tier": { "$ref": "#/components/schemas/SubscriptionTier" }
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["user_id", "tier"],
|
||||
|
||||
Reference in New Issue
Block a user