feat(platform): add daily-only vs daily+weekly reset option for rate limits

Collapse the reset button into the spending/usage display and add a
dropdown to choose between "Reset daily only" (default) and "Reset
daily + weekly". Backend accepts a new `reset_weekly` boolean parameter
on the reset endpoint; when false only the daily Redis key is deleted.
This commit is contained in:
Zamil Majdy
2026-03-26 10:34:11 +07:00
parent 925830de5a
commit c0687500a6
8 changed files with 110 additions and 36 deletions

View File

@@ -65,13 +65,17 @@ async def get_user_rate_limit(
)
async def reset_user_rate_limit(
user_id: str = Body(embed=True),
reset_weekly: bool = Body(False, embed=True),
admin_user_id: str = Security(get_user_id),
) -> UserRateLimitResponse:
"""Reset a user's daily and weekly usage counters to zero. Admin-only."""
logger.info(f"Admin {admin_user_id} resetting rate limit for user {user_id}")
"""Reset a user's daily usage counter (and optionally weekly). Admin-only."""
logger.info(
f"Admin {admin_user_id} resetting rate limit for user {user_id} "
f"(reset_weekly={reset_weekly})"
)
try:
await reset_user_usage(user_id)
await reset_user_usage(user_id, reset_weekly=reset_weekly)
except Exception as e:
logger.exception(f"Failed to reset user usage: {e}")
raise HTTPException(status_code=500, detail="Failed to reset usage") from e

View File

@@ -77,12 +77,52 @@ def test_get_rate_limit(
)
def test_reset_user_usage(
def test_reset_user_usage_daily_only(
mocker: pytest_mock.MockerFixture,
configured_snapshot: Snapshot,
target_user_id: str,
) -> None:
"""Test resetting a user's usage counters to zero."""
"""Test resetting only daily usage (default behaviour)."""
mock_reset = mocker.patch(
f"{_MOCK_MODULE}.reset_user_usage",
new_callable=AsyncMock,
)
mocker.patch(
f"{_MOCK_MODULE}.get_global_rate_limits",
new_callable=AsyncMock,
return_value=(2_500_000, 12_500_000),
)
mocker.patch(
f"{_MOCK_MODULE}.get_usage_status",
new_callable=AsyncMock,
return_value=_mock_usage_status(daily_used=0, weekly_used=3_000_000),
)
response = client.post(
"/admin/rate_limit/reset",
json={"user_id": target_user_id},
)
assert response.status_code == 200
data = response.json()
assert data["daily_tokens_used"] == 0
# Weekly is untouched
assert data["weekly_tokens_used"] == 3_000_000
mock_reset.assert_awaited_once_with(target_user_id, reset_weekly=False)
configured_snapshot.assert_match(
json.dumps(data, indent=2, sort_keys=True) + "\n",
"reset_user_usage_daily_only",
)
def test_reset_user_usage_daily_and_weekly(
mocker: pytest_mock.MockerFixture,
configured_snapshot: Snapshot,
target_user_id: str,
) -> None:
"""Test resetting both daily and weekly usage."""
mock_reset = mocker.patch(
f"{_MOCK_MODULE}.reset_user_usage",
new_callable=AsyncMock,
@@ -100,7 +140,7 @@ def test_reset_user_usage(
response = client.post(
"/admin/rate_limit/reset",
json={"user_id": target_user_id},
json={"user_id": target_user_id, "reset_weekly": True},
)
assert response.status_code == 200
@@ -108,11 +148,11 @@ def test_reset_user_usage(
assert data["daily_tokens_used"] == 0
assert data["weekly_tokens_used"] == 0
mock_reset.assert_awaited_once_with(target_user_id)
mock_reset.assert_awaited_once_with(target_user_id, reset_weekly=True)
configured_snapshot.assert_match(
json.dumps(data, indent=2, sort_keys=True) + "\n",
"reset_user_usage",
"reset_user_usage_daily_and_weekly",
)

View File

@@ -269,18 +269,19 @@ async def get_global_rate_limits(
return daily, weekly
async def reset_user_usage(user_id: str) -> None:
"""Reset a user's daily and weekly usage counters.
async def reset_user_usage(user_id: str, *, reset_weekly: bool = False) -> None:
"""Reset a user's usage counters.
Deletes the Redis keys for the current daily and weekly windows.
Always deletes the daily Redis key. When *reset_weekly* is ``True``,
the weekly key is deleted as well.
"""
now = datetime.now(UTC)
keys_to_delete = [_daily_key(user_id, now=now)]
if reset_weekly:
keys_to_delete.append(_weekly_key(user_id, now=now))
try:
redis = await get_redis_async()
await redis.delete(
_daily_key(user_id, now=now),
_weekly_key(user_id, now=now),
)
await redis.delete(*keys_to_delete)
except (RedisError, ConnectionError, OSError):
logger.warning("Redis unavailable for resetting user usage")
raise

View File

@@ -0,0 +1,7 @@
{
"daily_token_limit": 2500000,
"daily_tokens_used": 0,
"user_id": "5e53486c-cf57-477e-ba2a-cb02dc828e1c",
"weekly_token_limit": 12500000,
"weekly_tokens_used": 3000000
}

View File

@@ -39,38 +39,31 @@ function UsageBar({ used, limit }: { used: number; limit: number }) {
interface Props {
data: UserRateLimitResponse;
onReset: () => Promise<void>;
onReset: (resetWeekly: boolean) => Promise<void>;
}
export function RateLimitDisplay({ data, onReset }: Props) {
const [isResetting, setIsResetting] = useState(false);
const [resetWeekly, setResetWeekly] = useState(false);
async function handleReset() {
setIsResetting(true);
try {
await onReset();
await onReset(resetWeekly);
} finally {
setIsResetting(false);
}
}
const nothingToReset = resetWeekly
? data.daily_tokens_used === 0 && data.weekly_tokens_used === 0
: data.daily_tokens_used === 0;
return (
<div className="rounded-md border bg-white p-6">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold">
Rate Limits for {data.user_id}
</h2>
<Button
variant="outline"
onClick={handleReset}
disabled={
isResetting ||
(data.daily_tokens_used === 0 && data.weekly_tokens_used === 0)
}
>
{isResetting ? "Resetting..." : "Reset Usage to Zero"}
</Button>
</div>
<h2 className="mb-4 text-lg font-semibold">
Rate Limits for {data.user_id}
</h2>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
@@ -88,6 +81,25 @@ export function RateLimitDisplay({ data, onReset }: Props) {
/>
</div>
</div>
<div className="mt-6 flex items-center gap-3 border-t pt-4">
<select
value={resetWeekly ? "both" : "daily"}
onChange={(e) => setResetWeekly(e.target.value === "both")}
className="rounded-md border px-3 py-1.5 text-sm"
disabled={isResetting}
>
<option value="daily">Reset daily only</option>
<option value="both">Reset daily + weekly</option>
</select>
<Button
variant="outline"
onClick={handleReset}
disabled={isResetting || nothingToReset}
>
{isResetting ? "Resetting..." : "Reset Usage"}
</Button>
</div>
</div>
);
}

View File

@@ -44,12 +44,13 @@ export function RateLimitManager() {
}
}
async function handleReset() {
async function handleReset(resetWeekly: boolean) {
if (!rateLimitData) return;
try {
const response = await postV2ResetUserRateLimitUsage({
user_id: rateLimitData.user_id,
reset_weekly: resetWeekly,
});
if (response.status !== 200) {
throw new Error("Failed to reset usage");
@@ -57,7 +58,9 @@ export function RateLimitManager() {
setRateLimitData(response.data);
toast({
title: "Success",
description: "User rate limit usage reset to zero.",
description: resetWeekly
? "Daily and weekly usage reset to zero."
: "Daily usage reset to zero.",
});
} catch (error) {
console.error("Error resetting rate limit:", error);

View File

@@ -1448,7 +1448,7 @@
"post": {
"tags": ["v2", "admin", "copilot", "admin"],
"summary": "Reset User Rate Limit Usage",
"description": "Reset a user's daily and weekly usage counters to zero. Admin-only.",
"description": "Reset a user's daily usage counter (and optionally weekly). Admin-only.",
"operationId": "postV2Reset user rate limit usage",
"requestBody": {
"content": {
@@ -8238,7 +8238,14 @@
"title": "Body_postV2Execute a preset"
},
"Body_postV2Reset_user_rate_limit_usage": {
"properties": { "user_id": { "type": "string", "title": "User Id" } },
"properties": {
"user_id": { "type": "string", "title": "User Id" },
"reset_weekly": {
"type": "boolean",
"title": "Reset Weekly",
"default": false
}
},
"type": "object",
"required": ["user_id"],
"title": "Body_postV2Reset user rate limit usage"