52 lines
1.2 KiB
TypeScript
52 lines
1.2 KiB
TypeScript
|
|
import type { Plan } from '@bmm/llm';
|
||
|
|
import { getRedis } from './redis.js';
|
||
|
|
|
||
|
|
const DAY_SEC = 24 * 60 * 60;
|
||
|
|
|
||
|
|
function todayKey(): string {
|
||
|
|
return new Date().toISOString().slice(0, 10);
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface RateLimitResult {
|
||
|
|
ok: boolean;
|
||
|
|
remaining: number;
|
||
|
|
resetIn: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Daily counter via Redis INCR. Atomic — no race window between read & write.
|
||
|
|
* First INCR (count === 1) sets the TTL so the key auto-rolls at midnight UTC.
|
||
|
|
*/
|
||
|
|
export async function checkDailyLimit(
|
||
|
|
scope: string,
|
||
|
|
userId: string,
|
||
|
|
max: number,
|
||
|
|
): Promise<RateLimitResult> {
|
||
|
|
const key = `ratelimit:${scope}:${userId}:${todayKey()}`;
|
||
|
|
const redis = getRedis();
|
||
|
|
const count = await redis.incr(key);
|
||
|
|
if (count === 1) await redis.expire(key, DAY_SEC);
|
||
|
|
const ttl = await redis.ttl(key);
|
||
|
|
return {
|
||
|
|
ok: count <= max,
|
||
|
|
remaining: Math.max(0, max - count),
|
||
|
|
resetIn: ttl > 0 ? ttl : DAY_SEC,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Per-tier daily limits on the two LLM-priced actions.
|
||
|
|
// Preview = ~€0.002-0.015/call · Build = ~€0.005-0.22/call.
|
||
|
|
export const PREVIEW_DAILY_LIMIT: Record<Plan, number> = {
|
||
|
|
hobby: 5,
|
||
|
|
pro: 40,
|
||
|
|
team: 150,
|
||
|
|
enterprise: 1000,
|
||
|
|
};
|
||
|
|
|
||
|
|
export const BUILD_DAILY_LIMIT: Record<Plan, number> = {
|
||
|
|
hobby: 3,
|
||
|
|
pro: 20,
|
||
|
|
team: 100,
|
||
|
|
enterprise: 500,
|
||
|
|
};
|