GenerAI / worldmonitor /server /_shared /direct-llm-quota.ts
amogaddy's picture
Integra World Monitor (AGPL-3.0, self-hosted) nello Space: pagina, menu, e arricchimento notizie per la AI (part 5)
20f83d9 verified
Raw
History Blame Contribute Delete
3.24 kB
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<string>([
'/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<string>([
'/api/chat-analyst',
]);
export const DIRECT_LLM_QUOTA_PATHS = new Set<string>([
...DIRECT_LLM_GATEWAY_QUOTA_PATHS,
...DIRECT_LLM_SELF_METERED_QUOTA_PATHS,
]);
export type DirectLlmQuotaReservation =
| { ok: true; newCount: number; rollback: () => Promise<void> }
| {
ok: false;
reason: 'cap-exceeded' | 'redis-unavailable';
floor?: number;
retryAfterSec: number;
};
export type DirectLlmQuotaPipeline = (
commands: Array<Array<string | number>>,
) => Promise<Array<{ result?: unknown }>>;
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<DirectLlmQuotaReservation> {
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<void> => {
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 };
}