import { getKeyPrefix } from './redis'; import { PRO_DAILY_QUOTA_TTL_SECONDS, secondsUntilUtcMidnight } from './pro-mcp-token'; export const DIRECT_LLM_DAILY_QUOTA_LIMIT = 50; export const DIRECT_LLM_REDIS_UNAVAILABLE_RETRY_AFTER_SECONDS = 30; export const DIRECT_LLM_GATEWAY_QUOTA_PATHS = new Set([ '/api/intelligence/v1/classify-event', '/api/intelligence/v1/deduct-situation', '/api/intelligence/v1/get-country-intel-brief', '/api/market/v1/analyze-stock', '/api/news/v1/summarize-article', ]); export const DIRECT_LLM_SELF_METERED_QUOTA_PATHS = new Set([ '/api/chat-analyst', ]); export const DIRECT_LLM_QUOTA_PATHS = new Set([ ...DIRECT_LLM_GATEWAY_QUOTA_PATHS, ...DIRECT_LLM_SELF_METERED_QUOTA_PATHS, ]); export type DirectLlmQuotaReservation = | { ok: true; newCount: number; rollback: () => Promise } | { ok: false; reason: 'cap-exceeded' | 'redis-unavailable'; floor?: number; retryAfterSec: number; }; export type DirectLlmQuotaPipeline = ( commands: Array>, ) => Promise>; export function directLlmDailyQuotaKey(userId: string, date?: Date): string { if (!userId) return ''; const d = date ?? new Date(); const yyyy = d.getUTCFullYear(); const mm = String(d.getUTCMonth() + 1).padStart(2, '0'); const dd = String(d.getUTCDate()).padStart(2, '0'); return `${getKeyPrefix()}llm:direct-usage:${userId}:${yyyy}-${mm}-${dd}`; } export async function reserveDirectLlmQuota(opts: { userId: string; pipeline: DirectLlmQuotaPipeline; date?: Date; }): Promise { const retryAfterSec = secondsUntilUtcMidnight(opts.date); const key = directLlmDailyQuotaKey(opts.userId, opts.date); if (!key) { return { ok: false, reason: 'redis-unavailable', retryAfterSec: DIRECT_LLM_REDIS_UNAVAILABLE_RETRY_AFTER_SECONDS, }; } let pipeResult: Array<{ result?: unknown }> | null; try { pipeResult = await opts.pipeline([ ['INCR', key], ['EXPIRE', key, PRO_DAILY_QUOTA_TTL_SECONDS], ]); } catch { pipeResult = null; } if (!pipeResult || !Array.isArray(pipeResult) || pipeResult.length === 0) { return { ok: false, reason: 'redis-unavailable', retryAfterSec: DIRECT_LLM_REDIS_UNAVAILABLE_RETRY_AFTER_SECONDS, }; } const incrRaw = pipeResult[0]?.result; const newCount = typeof incrRaw === 'number' ? incrRaw : Number(incrRaw); if (!Number.isFinite(newCount) || newCount < 1) { return { ok: false, reason: 'redis-unavailable', retryAfterSec: DIRECT_LLM_REDIS_UNAVAILABLE_RETRY_AFTER_SECONDS, }; } let rolledBack = false; const rollback = async (): Promise => { if (rolledBack) return; rolledBack = true; try { await opts.pipeline([['DECR', key]]); } catch { // Best-effort: over-counting by one is the cost-protection-correct direction. } }; if (newCount > DIRECT_LLM_DAILY_QUOTA_LIMIT) { await rollback(); return { ok: false, reason: 'cap-exceeded', floor: DIRECT_LLM_DAILY_QUOTA_LIMIT, retryAfterSec, }; } return { ok: true, newCount, rollback }; }