UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

184 lines (183 loc) 6.58 kB
/** * Per-Account Quota Tracking * * Captures Anthropic rate-limit / utilisation headers from proxy responses * and persists them to ~/.neurolink/account-quotas.json so the CLI can * display remaining session & weekly capacity per account. * * Hot-path design: parseQuotaHeaders is pure CPU (no I/O). saveAccountQuota * updates an in-memory cache and debounces disk writes so the request/response * path is never blocked by file I/O. */ import { dirname, join } from "path"; import { homedir } from "os"; import { promises as fs } from "fs"; // --------------------------------------------------------------------------- // Header parsing (pure CPU — no I/O, safe for hot path) // --------------------------------------------------------------------------- function getHeader(headers, name) { if (typeof headers.get === "function") { return headers.get(name) ?? undefined; } const rec = headers; if (rec[name] !== undefined) { return rec[name]; } const lower = name.toLowerCase(); for (const key of Object.keys(rec)) { if (key.toLowerCase() === lower) { return rec[key]; } } return undefined; } /** * Parse Anthropic rate-limit / quota headers into an `AccountQuota`. * Returns `null` when key headers are absent. * Pure computation — no I/O, no blocking. */ export function parseQuotaHeaders(headers) { // Anthropic prefixes all quota headers with "anthropic-ratelimit-" const P = "anthropic-ratelimit-"; const sessionUtilRaw = getHeader(headers, `${P}unified-5h-utilization`); const weeklyUtilRaw = getHeader(headers, `${P}unified-7d-utilization`); if (sessionUtilRaw === undefined || weeklyUtilRaw === undefined) { return null; } const sessionUsed = parseFloat(sessionUtilRaw); const weeklyUsed = parseFloat(weeklyUtilRaw); if (Number.isNaN(sessionUsed) || Number.isNaN(weeklyUsed)) { return null; } const sessionResetRaw = getHeader(headers, `${P}unified-5h-reset`); const weeklyResetRaw = getHeader(headers, `${P}unified-7d-reset`); const fallbackRaw = getHeader(headers, `${P}unified-fallback-percentage`); return { sessionUsed, sessionStatus: getHeader(headers, `${P}unified-5h-status`) ?? "unknown", sessionResetAt: sessionResetRaw ? parseInt(sessionResetRaw, 10) || 0 : 0, weeklyUsed, weeklyStatus: getHeader(headers, `${P}unified-7d-status`) ?? "unknown", weeklyResetAt: weeklyResetRaw ? parseInt(weeklyResetRaw, 10) || 0 : 0, fallbackPercentage: fallbackRaw ? parseFloat(fallbackRaw) || 0 : 0, overageStatus: getHeader(headers, `${P}unified-overage-status`) ?? "unknown", lastUpdated: Date.now(), }; } // --------------------------------------------------------------------------- // In-memory cache + debounced async persistence // --------------------------------------------------------------------------- const QUOTA_FILE = "account-quotas.json"; const FLUSH_INTERVAL_MS = 5_000; // write to disk at most every 5 seconds let memoryCache = {}; let cacheLoaded = false; let dirty = false; let flushTimer = null; /** Custom quota file path set via initAccountQuota(). */ let customQuotaFilePath = null; /** * Initialise the quota module with a custom file path. * When set, all reads/writes go to this path instead of the default * ~/.neurolink/account-quotas.json. Call before the first load/save. */ export function initAccountQuota(quotaFilePath) { customQuotaFilePath = quotaFilePath; // Cancel any pending flush from a previous configuration so it does not // write stale data to the new path. if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; } // Reset cache so the new path is picked up on next load memoryCache = {}; cacheLoaded = false; dirty = false; } function getQuotaFilePath() { return customQuotaFilePath ?? join(homedir(), ".neurolink", QUOTA_FILE); } async function ensureDir() { const filePath = getQuotaFilePath(); const dir = dirname(filePath); await fs.mkdir(dir, { recursive: true, mode: 0o700 }).catch(() => { // Non-fatal: directory may already exist }); } /** Flush the in-memory cache to disk (async, non-blocking). */ async function flushToDisk() { if (!dirty) { return; } try { // Snapshot before async I/O so we only clear dirty if nothing changed const snapshot = JSON.stringify(memoryCache, null, 2); await ensureDir(); const filePath = getQuotaFilePath(); const tmpPath = `${filePath}.tmp`; await fs.writeFile(tmpPath, snapshot, { mode: 0o600, }); await fs.rename(tmpPath, filePath); // Only clear dirty if the cache hasn't changed during the write if (JSON.stringify(memoryCache, null, 2) === snapshot) { dirty = false; } } catch { // Non-fatal — quota is best-effort telemetry } } function scheduleFlush() { if (flushTimer) { return; } // already scheduled flushTimer = setTimeout(() => { flushTimer = null; flushToDisk().catch(() => { // Non-fatal: quota persistence is best-effort }); }, FLUSH_INTERVAL_MS); // Don't prevent process exit if (flushTimer && typeof flushTimer === "object" && "unref" in flushTimer) { flushTimer.unref(); } } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /** * Load all persisted account quotas. * First call reads from disk; subsequent calls return the in-memory cache. */ export async function loadAccountQuotas() { if (cacheLoaded) { return { ...memoryCache }; } try { const raw = await fs.readFile(getQuotaFilePath(), "utf-8"); memoryCache = JSON.parse(raw); } catch { memoryCache = {}; } cacheLoaded = true; return { ...memoryCache }; } /** * Load quota for a single account. */ export async function loadAccountQuota(accountKey) { const all = await loadAccountQuotas(); return all[accountKey] ?? null; } /** * Update quota for a single account. * Updates in-memory cache immediately (non-blocking), * then debounces the disk write to every 5 seconds. */ export async function saveAccountQuota(accountKey, quota) { memoryCache[accountKey] = quota; cacheLoaded = true; dirty = true; scheduleFlush(); }