feat(platform): admin rate limit check and reset with LD-configurable global limits (#12566)

## Why
Admins need visibility into per-user CoPilot rate limit usage and the
ability to reset a user's counters when needed (e.g., after a false
positive or for debugging). Additionally, the global rate limits were
hardcoded deploy-time constants with no way to adjust without
redeploying.

## What
- Admin endpoints to **check** a user's current rate limit usage and
**reset** their daily/weekly counters to zero
- Global rate limits are now **LaunchDarkly-configurable** via
`copilot-daily-token-limit` and `copilot-weekly-token-limit` flags,
falling back to existing `ChatConfig` values
- Frontend admin page at `/admin/rate-limits` with user lookup, usage
visualization, and reset capability
- Chat routes updated to source global limits from LD flags

## How
- **Backend**: Added `reset_user_usage()` to `rate_limit.py` that
deletes Redis usage keys. New admin routes in
`rate_limit_admin_routes.py` (GET `/api/copilot/admin/rate_limit` and
POST `/api/copilot/admin/rate_limit/reset`). Added
`COPILOT_DAILY_TOKEN_LIMIT` and `COPILOT_WEEKLY_TOKEN_LIMIT` to the
`Flag` enum. Chat routes use `_get_global_rate_limits()` helper that
checks LD first.
- **Frontend**: New `/admin/rate-limits` page with `RateLimitManager`
(user lookup) and `RateLimitDisplay` (usage bars + reset button). Added
`getUserRateLimit` and `resetUserRateLimit` to `BackendAPI` client.

## Test plan
- [x] Backend: 4 tests covering get, reset, redis failure, and
admin-only access
- [ ] Manual: Look up a user's rate limits in the admin UI
- [ ] Manual: Reset a user's usage counters
- [ ] Manual: Verify LD flag overrides are respected for global limits
This commit is contained in:
Zamil Majdy
2026-03-26 15:29:40 +07:00
committed by GitHub
parent a347c274b7
commit d677978c90
15 changed files with 764 additions and 7 deletions

View File

@@ -0,0 +1,94 @@
"""Admin endpoints for checking and resetting user CoPilot rate limit usage."""
import logging
from autogpt_libs.auth import get_user_id, requires_admin_user
from fastapi import APIRouter, Body, HTTPException, Security
from pydantic import BaseModel
from backend.copilot.config import ChatConfig
from backend.copilot.rate_limit import (
get_global_rate_limits,
get_usage_status,
reset_user_usage,
)
logger = logging.getLogger(__name__)
config = ChatConfig()
router = APIRouter(
prefix="/admin",
tags=["copilot", "admin"],
dependencies=[Security(requires_admin_user)],
)
class UserRateLimitResponse(BaseModel):
user_id: str
daily_token_limit: int
weekly_token_limit: int
daily_tokens_used: int
weekly_tokens_used: int
@router.get(
"/rate_limit",
response_model=UserRateLimitResponse,
summary="Get User Rate Limit",
)
async def get_user_rate_limit(
user_id: str,
admin_user_id: str = Security(get_user_id),
) -> UserRateLimitResponse:
"""Get a user's current usage and effective rate limits. Admin-only."""
logger.info(f"Admin {admin_user_id} checking rate limit for user {user_id}")
daily_limit, weekly_limit = await get_global_rate_limits(
user_id, config.daily_token_limit, config.weekly_token_limit
)
usage = await get_usage_status(user_id, daily_limit, weekly_limit)
return UserRateLimitResponse(
user_id=user_id,
daily_token_limit=daily_limit,
weekly_token_limit=weekly_limit,
daily_tokens_used=usage.daily.used,
weekly_tokens_used=usage.weekly.used,
)
@router.post(
"/rate_limit/reset",
response_model=UserRateLimitResponse,
summary="Reset User Rate Limit Usage",
)
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 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, reset_weekly=reset_weekly)
except Exception as e:
logger.exception("Failed to reset user usage")
raise HTTPException(status_code=500, detail="Failed to reset usage") from e
daily_limit, weekly_limit = await get_global_rate_limits(
user_id, config.daily_token_limit, config.weekly_token_limit
)
usage = await get_usage_status(user_id, daily_limit, weekly_limit)
return UserRateLimitResponse(
user_id=user_id,
daily_token_limit=daily_limit,
weekly_token_limit=weekly_limit,
daily_tokens_used=usage.daily.used,
weekly_tokens_used=usage.weekly.used,
)

View File

@@ -0,0 +1,189 @@
import json
from unittest.mock import AsyncMock
import fastapi
import fastapi.testclient
import pytest
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, UsageWindow
from .rate_limit_admin_routes import router as rate_limit_admin_router
app = fastapi.FastAPI()
app.include_router(rate_limit_admin_router)
client = fastapi.testclient.TestClient(app)
_MOCK_MODULE = "backend.api.features.admin.rate_limit_admin_routes"
@pytest.fixture(autouse=True)
def setup_app_admin_auth(mock_jwt_admin):
"""Setup admin auth overrides for all tests in this module"""
app.dependency_overrides[get_jwt_payload] = mock_jwt_admin["get_jwt_payload"]
yield
app.dependency_overrides.clear()
def _mock_usage_status(
daily_used: int = 500_000, weekly_used: int = 3_000_000
) -> CoPilotUsageStatus:
from datetime import UTC, datetime, timedelta
now = datetime.now(UTC)
return CoPilotUsageStatus(
daily=UsageWindow(
used=daily_used, limit=2_500_000, resets_at=now + timedelta(hours=6)
),
weekly=UsageWindow(
used=weekly_used, limit=12_500_000, resets_at=now + timedelta(days=3)
),
)
def test_get_rate_limit(
mocker: pytest_mock.MockerFixture,
configured_snapshot: Snapshot,
target_user_id: str,
) -> None:
"""Test getting rate limit and usage for a user."""
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(),
)
response = client.get("/admin/rate_limit", params={"user_id": target_user_id})
assert response.status_code == 200
data = response.json()
assert data["user_id"] == target_user_id
assert data["daily_token_limit"] == 2_500_000
assert data["weekly_token_limit"] == 12_500_000
assert data["daily_tokens_used"] == 500_000
assert data["weekly_tokens_used"] == 3_000_000
configured_snapshot.assert_match(
json.dumps(data, indent=2, sort_keys=True) + "\n",
"get_rate_limit",
)
def test_reset_user_usage_daily_only(
mocker: pytest_mock.MockerFixture,
configured_snapshot: Snapshot,
target_user_id: str,
) -> None:
"""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,
)
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=0),
)
response = client.post(
"/admin/rate_limit/reset",
json={"user_id": target_user_id, "reset_weekly": True},
)
assert response.status_code == 200
data = response.json()
assert data["daily_tokens_used"] == 0
assert data["weekly_tokens_used"] == 0
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_daily_and_weekly",
)
def test_reset_user_usage_redis_failure(
mocker: pytest_mock.MockerFixture,
target_user_id: str,
) -> None:
"""Test that Redis failure on reset returns 500."""
mocker.patch(
f"{_MOCK_MODULE}.reset_user_usage",
new_callable=AsyncMock,
side_effect=Exception("Redis connection refused"),
)
response = client.post(
"/admin/rate_limit/reset",
json={"user_id": target_user_id},
)
assert response.status_code == 500
def test_admin_endpoints_require_admin_role(mock_jwt_user) -> None:
"""Test that rate limit admin endpoints require admin role."""
app.dependency_overrides[get_jwt_payload] = mock_jwt_user["get_jwt_payload"]
response = client.get("/admin/rate_limit", params={"user_id": "test"})
assert response.status_code == 403
response = client.post(
"/admin/rate_limit/reset",
json={"user_id": "test"},
)
assert response.status_code == 403

View File

@@ -31,6 +31,7 @@ from backend.copilot.rate_limit import (
CoPilotUsageStatus,
RateLimitExceeded,
check_rate_limit,
get_global_rate_limits,
get_usage_status,
)
from backend.copilot.response_model import StreamError, StreamFinish, StreamHeartbeat
@@ -64,14 +65,14 @@ from backend.data.understanding import get_business_understanding
from backend.data.workspace import get_or_create_workspace
from backend.util.exceptions import NotFoundError
logger = logging.getLogger(__name__)
config = ChatConfig()
_UUID_RE = re.compile(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.I
)
logger = logging.getLogger(__name__)
async def _validate_and_get_session(
session_id: str,
@@ -422,11 +423,15 @@ async def get_copilot_usage(
"""Get CoPilot usage status for the authenticated user.
Returns current token usage vs limits for daily and weekly windows.
Global defaults sourced from LaunchDarkly (falling back to config).
"""
daily_limit, weekly_limit = await get_global_rate_limits(
user_id, config.daily_token_limit, config.weekly_token_limit
)
return await get_usage_status(
user_id=user_id,
daily_token_limit=config.daily_token_limit,
weekly_token_limit=config.weekly_token_limit,
daily_token_limit=daily_limit,
weekly_token_limit=weekly_limit,
)
@@ -527,12 +532,16 @@ async def stream_chat_post(
# Pre-turn rate limit check (token-based).
# check_rate_limit short-circuits internally when both limits are 0.
# Global defaults sourced from LaunchDarkly, falling back to config.
if user_id:
try:
daily_limit, weekly_limit = await get_global_rate_limits(
user_id, config.daily_token_limit, config.weekly_token_limit
)
await check_rate_limit(
user_id=user_id,
daily_token_limit=config.daily_token_limit,
weekly_token_limit=config.weekly_token_limit,
daily_token_limit=daily_limit,
weekly_token_limit=weekly_limit,
)
except RateLimitExceeded as e:
raise HTTPException(status_code=429, detail=str(e)) from e

View File

@@ -18,6 +18,7 @@ from prisma.errors import PrismaError
import backend.api.features.admin.credit_admin_routes
import backend.api.features.admin.execution_analytics_routes
import backend.api.features.admin.rate_limit_admin_routes
import backend.api.features.admin.store_admin_routes
import backend.api.features.builder
import backend.api.features.builder.routes
@@ -318,6 +319,11 @@ app.include_router(
tags=["v2", "admin"],
prefix="/api/executions",
)
app.include_router(
backend.api.features.admin.rate_limit_admin_routes.router,
tags=["v2", "admin"],
prefix="/api/copilot",
)
app.include_router(
backend.api.features.executions.review.routes.router,
tags=["v2", "executions", "review"],

View File

@@ -231,6 +231,67 @@ async def record_token_usage(
)
async def get_global_rate_limits(
user_id: str,
config_daily: int,
config_weekly: int,
) -> tuple[int, int]:
"""Resolve global rate limits from LaunchDarkly, falling back to config.
Args:
user_id: User ID for LD flag evaluation context.
config_daily: Fallback daily limit from ChatConfig.
config_weekly: Fallback weekly limit from ChatConfig.
Returns:
(daily_token_limit, weekly_token_limit) tuple.
"""
# Lazy import to avoid circular dependency:
# rate_limit -> feature_flag -> settings -> ... -> rate_limit
from backend.util.feature_flag import Flag, get_feature_flag_value
daily_raw = await get_feature_flag_value(
Flag.COPILOT_DAILY_TOKEN_LIMIT.value, user_id, config_daily
)
weekly_raw = await get_feature_flag_value(
Flag.COPILOT_WEEKLY_TOKEN_LIMIT.value, user_id, config_weekly
)
try:
daily = max(0, int(daily_raw))
except (TypeError, ValueError):
logger.warning("Invalid LD value for daily token limit: %r", daily_raw)
daily = config_daily
try:
weekly = max(0, int(weekly_raw))
except (TypeError, ValueError):
logger.warning("Invalid LD value for weekly token limit: %r", weekly_raw)
weekly = config_weekly
return daily, weekly
async def reset_user_usage(user_id: str, *, reset_weekly: bool = False) -> None:
"""Reset a user's usage counters.
Always deletes the daily Redis key. When *reset_weekly* is ``True``,
the weekly key is deleted as well.
Unlike read paths (``get_usage_status``, ``check_rate_limit``) which
fail-open on Redis errors, resets intentionally re-raise so the caller
knows the operation did not succeed. A silent failure here would leave
the admin believing the counters were zeroed when they were not.
"""
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(*keys_to_delete)
except (RedisError, ConnectionError, OSError):
logger.warning("Redis unavailable for resetting user usage")
raise
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------

View File

@@ -39,6 +39,8 @@ class Flag(str, Enum):
ENABLE_PLATFORM_PAYMENT = "enable-platform-payment"
CHAT = "chat"
COPILOT_SDK = "copilot-sdk"
COPILOT_DAILY_TOKEN_LIMIT = "copilot-daily-token-limit"
COPILOT_WEEKLY_TOKEN_LIMIT = "copilot-weekly-token-limit"
def is_configured() -> bool:

View File

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

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": 0
}

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

@@ -1,5 +1,6 @@
import { Sidebar } from "@/components/__legacy__/Sidebar";
import { Users, DollarSign, UserSearch, FileText } from "lucide-react";
import { Gauge } from "@phosphor-icons/react/dist/ssr";
import { IconSliders } from "@/components/__legacy__/ui/icons";
@@ -21,6 +22,11 @@ const sidebarLinkGroups = [
href: "/admin/impersonation",
icon: <UserSearch className="h-6 w-6" />,
},
{
text: "Rate Limits",
href: "/admin/rate-limits",
icon: <Gauge className="h-6 w-6" />,
},
{
text: "Execution Analytics",
href: "/admin/execution-analytics",

View File

@@ -0,0 +1,114 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/atoms/Button/Button";
import type { UserRateLimitResponse } from "@/app/api/__generated__/models/userRateLimitResponse";
function formatTokens(tokens: number): string {
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(0)}K`;
return tokens.toString();
}
function UsageBar({ used, limit }: { used: number; limit: number }) {
if (limit === 0) {
return <span className="text-sm text-gray-500">Unlimited</span>;
}
const pct = Math.min((used / limit) * 100, 100);
const color =
pct >= 90 ? "bg-red-500" : pct >= 70 ? "bg-yellow-500" : "bg-green-500";
return (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span>{formatTokens(used)} used</span>
<span>{formatTokens(limit)} limit</span>
</div>
<div className="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div
className={`h-2 rounded-full ${color}`}
style={{ width: `${pct}%` }}
/>
</div>
<div className="text-right text-xs text-gray-500">
{pct.toFixed(1)}% used
</div>
</div>
);
}
interface Props {
data: UserRateLimitResponse;
onReset: (resetWeekly: boolean) => Promise<void>;
}
export function RateLimitDisplay({ data, onReset }: Props) {
const [isResetting, setIsResetting] = useState(false);
const [resetWeekly, setResetWeekly] = useState(false);
async function handleReset() {
const msg = resetWeekly
? "Reset both daily and weekly usage counters to zero?"
: "Reset daily usage counter to zero?";
if (!window.confirm(msg)) return;
setIsResetting(true);
try {
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 dark:bg-gray-900">
<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">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Daily Usage
</h3>
<UsageBar
used={data.daily_tokens_used}
limit={data.daily_token_limit}
/>
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Weekly Usage
</h3>
<UsageBar
used={data.weekly_tokens_used}
limit={data.weekly_token_limit}
/>
</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 bg-white px-3 py-1.5 text-sm dark:bg-gray-800 dark:text-gray-200"
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

@@ -0,0 +1,104 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/__legacy__/ui/input";
import { Label } from "@/components/__legacy__/ui/label";
import { MagnifyingGlass } from "@phosphor-icons/react";
import { useToast } from "@/components/molecules/Toast/use-toast";
import type { UserRateLimitResponse } from "@/app/api/__generated__/models/userRateLimitResponse";
import {
getV2GetUserRateLimit,
postV2ResetUserRateLimitUsage,
} from "@/app/api/__generated__/endpoints/admin/admin";
import { RateLimitDisplay } from "./RateLimitDisplay";
export function RateLimitManager() {
const { toast } = useToast();
const [userIdInput, setUserIdInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [rateLimitData, setRateLimitData] =
useState<UserRateLimitResponse | null>(null);
async function handleLookup() {
const trimmed = userIdInput.trim();
if (!trimmed) return;
setIsLoading(true);
try {
const response = await getV2GetUserRateLimit({ user_id: trimmed });
if (response.status !== 200) {
throw new Error("Failed to fetch rate limit");
}
setRateLimitData(response.data);
} catch (error) {
console.error("Error fetching rate limit:", error);
toast({
title: "Error",
description: "Failed to fetch user rate limit. Check the user ID.",
variant: "destructive",
});
setRateLimitData(null);
} finally {
setIsLoading(false);
}
}
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");
}
setRateLimitData(response.data);
toast({
title: "Success",
description: resetWeekly
? "Daily and weekly usage reset to zero."
: "Daily usage reset to zero.",
});
} catch (error) {
console.error("Error resetting rate limit:", error);
toast({
title: "Error",
description: "Failed to reset rate limit usage.",
variant: "destructive",
});
}
}
return (
<div className="space-y-6">
<div className="rounded-md border bg-white p-6 dark:bg-gray-900">
<Label htmlFor="userId" className="mb-2 block text-sm font-medium">
User ID
</Label>
<div className="flex items-center gap-2">
<Input
id="userId"
placeholder="Enter user ID to look up rate limits..."
value={userIdInput}
onChange={(e) => setUserIdInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleLookup()}
/>
<Button
variant="outline"
onClick={handleLookup}
disabled={isLoading || !userIdInput.trim()}
>
{isLoading ? "Loading..." : <MagnifyingGlass size={16} />}
</Button>
</div>
</div>
{rateLimitData && (
<RateLimitDisplay data={rateLimitData} onReset={handleReset} />
)}
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { withRoleAccess } from "@/lib/withRoleAccess";
import { RateLimitManager } from "./components/RateLimitManager";
function RateLimitsDashboard() {
return (
<div className="mx-auto p-6">
<div className="flex flex-col gap-4">
<div>
<h1 className="text-3xl font-bold">User Rate Limits</h1>
<p className="text-gray-500">
Check and manage CoPilot rate limits per user
</p>
</div>
<RateLimitManager />
</div>
</div>
);
}
export default async function RateLimitsDashboardPage() {
"use server";
const withAdminAccess = await withRoleAccess(["admin"]);
const ProtectedDashboard = await withAdminAccess(RateLimitsDashboard);
return <ProtectedDashboard />;
}

View File

@@ -1386,7 +1386,7 @@
"get": {
"tags": ["v2", "chat", "chat"],
"summary": "Get Copilot Usage",
"description": "Get CoPilot usage status for the authenticated user.\n\nReturns current token usage vs limits for daily and weekly windows.",
"description": "Get CoPilot usage status for the authenticated user.\n\nReturns current token usage vs limits for daily and weekly windows.\nGlobal defaults sourced from LaunchDarkly (falling back to config).",
"operationId": "getV2GetCopilotUsage",
"responses": {
"200": {
@@ -1404,6 +1404,88 @@
"security": [{ "HTTPBearerJWT": [] }]
}
},
"/api/copilot/admin/rate_limit": {
"get": {
"tags": ["v2", "admin", "copilot", "admin"],
"summary": "Get User Rate Limit",
"description": "Get a user's current usage and effective rate limits. Admin-only.",
"operationId": "getV2Get user rate limit",
"security": [{ "HTTPBearerJWT": [] }],
"parameters": [
{
"name": "user_id",
"in": "query",
"required": true,
"schema": { "type": "string", "title": "User Id" }
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRateLimitResponse"
}
}
}
},
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/copilot/admin/rate_limit/reset": {
"post": {
"tags": ["v2", "admin", "copilot", "admin"],
"summary": "Reset User Rate Limit Usage",
"description": "Reset a user's daily usage counter (and optionally weekly). Admin-only.",
"operationId": "postV2Reset user rate limit usage",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_postV2Reset_user_rate_limit_usage"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRateLimitResponse"
}
}
}
},
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
},
"security": [{ "HTTPBearerJWT": [] }]
}
},
"/api/credits": {
"get": {
"tags": ["v1", "credits"],
@@ -8155,6 +8237,19 @@
"type": "object",
"title": "Body_postV2Execute a preset"
},
"Body_postV2Reset_user_rate_limit_usage": {
"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"
},
"Body_postV2Upload_submission_media": {
"properties": {
"file": { "type": "string", "format": "binary", "title": "File" }
@@ -14533,6 +14628,36 @@
"required": ["provider", "username", "password"],
"title": "UserPasswordCredentials"
},
"UserRateLimitResponse": {
"properties": {
"user_id": { "type": "string", "title": "User Id" },
"daily_token_limit": {
"type": "integer",
"title": "Daily Token Limit"
},
"weekly_token_limit": {
"type": "integer",
"title": "Weekly Token Limit"
},
"daily_tokens_used": {
"type": "integer",
"title": "Daily Tokens Used"
},
"weekly_tokens_used": {
"type": "integer",
"title": "Weekly Tokens Used"
}
},
"type": "object",
"required": [
"user_id",
"daily_token_limit",
"weekly_token_limit",
"daily_tokens_used",
"weekly_tokens_used"
],
"title": "UserRateLimitResponse"
},
"UserReadiness": {
"properties": {
"has_all_credentials": {

View File

@@ -964,6 +964,7 @@ export type AddUserCreditsResponse = {
new_balance: number;
transaction_key: string;
};
const _stringFormatToDataTypeMap: Partial<Record<string, DataType>> = {
date: DataType.DATE,
time: DataType.TIME,