Перейти к основному содержимому

Per-user daily token cap that prevents cost-amplification abuse. A single authenticated user cannot exhaust the AI quota for all other users by running many expensive requests.


Config

DAILY_TOKEN_CAP in src/lib/server/ai/config.ts caps input + output tokens combined, per user, per UTC day. The number is not repeated here — at the value it currently holds, spend lands around $1–2 per user per day on premium models, and a doc that restates it is a doc that goes stale the first time it is tuned.


Redis Key Shape

ai:budget:{userId}:{YYYY-MM-DD}

TTL: 25 hours (TTL_SECONDS = 25 * 60 * 60). The extra hour past midnight ensures the key is still readable in the final minutes of a UTC day regardless of clock skew between app instances.

The {YYYY-MM-DD} segment is derived from new Date().toISOString().slice(0, 10) — always UTC.


API ($lib/server/ai/budget.ts)

checkUserBudget(userId)

Call at request entry — before any expensive model invocation:

import { checkUserBudget } from '$lib/server/ai/budget';

const budget = await checkUserBudget(userId);
if (!budget.allowed) return decisionResponse(budget);

Returns a Decision. On denial, retryAfterMs is set to milliseconds until UTC midnight so the client can show an accurate reset time.

Redis-unavailable behavior:

Environment Redis missing
Dev Passthrough (always allowed)
Production Denied — budget check unavailable is treated as over-limit

chargeTokens(userId, tokens)

Call once the model is done, while the response is still open (the chatbot's post-text stage runs it alongside the message backfill, before finish):

import { chargeTokens } from '$lib/server/ai/budget';

// After the model's total usage is known, before the stream closes:
await chargeTokens(userId, usage.totalTokens);

One pipelined Redis round trip: INCRBY + EXPIRE (the TTL retires the key with its day). It sits on the answer path between the last token and finish, so it is one hop, not two.


Check-Then-Charge Caveat (v1)

The gate and charge are not atomic. A burst of N parallel requests can each pass checkUserBudget before any has called chargeTokens. The daily total can overshoot by up to N × MAX_TOKENS before the cap engages.

At current rates the worst-case overshoot is $0.20–0.50 — acceptable for v1. Upgrade to atomic pre-charge (reserve tokens before generation, reconcile after) if abuse data warrants it.


BOT_DETECTION_MODE Interaction

The token budget does not read BOT_DETECTION_MODE — it is always enforced when Redis is available. The mode flag is scoped to the captcha layer only.


Where Enforced

  • Shared AI entry guard (src/lib/server/ai/guard.ts, guardAiRequest): checkUserBudget runs in the auth → aiConfigured → (rate-limit ∥ budget) preamble shared by the AI routes (/api/ai/chatbot, /api/ai/deskbot, /api/ai/images/[id]/analyze), before the AI work starts. The limiter and the budget are read concurrently; when both deny, the rate-limit response answers. On a budget denial alone, returns decisionResponse(budget) (429).
  • Chat orchestrator ($lib/server/ai/chat-orchestrator.ts): chargeTokens(userId, inputTokens + outputTokens) in the chatbot's post-text stage (afterText, together with the message backfill and the token-total refresh, all awaited before finish) and in the deskbot/fallback onFinish.

The gate sits in the entry guard because the chatbot grounds every turn (forced tool-provider routing + per-turn retrieval), so the cost floor per request is higher — the budget is checked before any model work begins, and the single guard keeps the rejection identical across every AI route.


← Back to Blueprint

Думаете, этот паттерн можно сделать лучше? Расскажите как.

Оставить отзыв