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>
75 lines
2.6 KiB
Python
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(),
|
|
}
|