Files
personal-ai-assistant/backend/app/services/usage_service.py
dolgolyov.alexei d86d53f473 Phase 10: Per-User Rate Limits — messages + tokens, quota UI, admin usage
Backend:
- max_ai_messages_per_day + max_ai_tokens_per_day on User model (nullable, override)
- Migration 008: add columns + seed default settings (100 msgs, 500K tokens)
- usage_service: count today's messages + tokens, check quota, get limits
- GET /chats/quota returns usage vs limits + reset time
- POST /chats/{id}/messages checks quota before streaming (429 if exceeded)
- Admin user schemas expose both limit fields
- GET /admin/usage returns per-user daily message + token counts
- admin_user_service allows updating both limit fields

Frontend:
- Chat header shows "X/Y messages · XK/YK tokens" with red highlight at limit
- Quota refreshes every 30s via TanStack Query
- Admin usage page with table: user, messages today, tokens today
- Route + sidebar entry for admin usage
- English + Russian translations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:44:51 +03:00

75 lines
2.6 KiB
Python

import uuid
from datetime import datetime, timezone
from sqlalchemy import func, select, cast, Integer
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.chat import Chat
from app.models.message import Message
from app.models.user import User
from app.services.setting_service import get_setting_value
def _today_start() -> datetime:
return datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
async def get_user_message_count_today(db: AsyncSession, user_id: uuid.UUID) -> int:
result = await db.scalar(
select(func.count())
.select_from(Message)
.join(Chat, Message.chat_id == Chat.id)
.where(Chat.user_id == user_id, Message.role == "user", Message.created_at >= _today_start())
)
return result or 0
async def get_user_token_count_today(db: AsyncSession, user_id: uuid.UUID) -> int:
"""Sum input_tokens + output_tokens from assistant message metadata today."""
result = await db.execute(
select(Message.metadata_)
.join(Chat, Message.chat_id == Chat.id)
.where(
Chat.user_id == user_id,
Message.role == "assistant",
Message.created_at >= _today_start(),
Message.metadata_.isnot(None),
)
)
total = 0
for (meta,) in result:
if meta and isinstance(meta, dict):
total += int(meta.get("input_tokens", 0)) + int(meta.get("output_tokens", 0))
return total
async def get_user_daily_limits(db: AsyncSession, user: User) -> dict:
msg_limit = user.max_ai_messages_per_day
if msg_limit is None:
msg_limit = int(await get_setting_value(db, "default_max_ai_messages_per_day", 100))
token_limit = user.max_ai_tokens_per_day
if token_limit is None:
token_limit = int(await get_setting_value(db, "default_max_ai_tokens_per_day", 500000))
return {"message_limit": msg_limit, "token_limit": token_limit}
async def check_user_quota(db: AsyncSession, user: User) -> dict:
"""Check quota and return status. Raises nothing — caller decides."""
limits = await get_user_daily_limits(db, user)
msg_count = await get_user_message_count_today(db, user.id)
token_count = await get_user_token_count_today(db, user.id)
tomorrow = _today_start().replace(day=_today_start().day + 1)
return {
"messages_used": msg_count,
"message_limit": limits["message_limit"],
"tokens_used": token_count,
"token_limit": limits["token_limit"],
"messages_exceeded": msg_count >= limits["message_limit"],
"tokens_exceeded": token_count >= limits["token_limit"],
"resets_at": tomorrow.isoformat(),
}