UNPKG

claude-gemini-multimodal-bridge

Version:

Enterprise-grade AI integration bridge connecting Claude Code, Gemini CLI, and Google AI Studio with intelligent routing and advanced multimodal processing capabilities

236 lines (235 loc) 9.52 kB
import { logger } from './logger.js'; export class QuotaMonitor { FREE_TIER_LIMITS = { requests_per_minute: 15, requests_per_day: 1500, tokens_per_minute: 1000000, tokens_per_day: 1500000000, }; PAID_TIER_LIMITS = { requests_per_minute: 2000, requests_per_day: -1, tokens_per_minute: 10000000, tokens_per_day: -1, }; usage = { requests_today: 0, requests_this_minute: 0, tokens_today: 0, tokens_this_minute: 0, last_reset_daily: Date.now(), last_reset_minute: Date.now(), }; WARNING_THRESHOLDS = { requests_daily: 0.8, requests_minute: 0.9, tokens_daily: 0.8, tokens_minute: 0.9, }; isPaidTier = false; constructor(isPaidTier = false) { this.isPaidTier = isPaidTier; this.loadStoredUsage(); this.setupPeriodicReset(); } trackRequest(tokensUsed = 0) { this.resetCountersIfNeeded(); this.usage.requests_today++; this.usage.requests_this_minute++; this.usage.tokens_today += tokensUsed; this.usage.tokens_this_minute += tokensUsed; this.saveUsage(); this.checkAndWarnLimits(); } canMakeRequest(estimatedTokens = 1000) { this.resetCountersIfNeeded(); const limits = this.isPaidTier ? this.PAID_TIER_LIMITS : this.FREE_TIER_LIMITS; if (limits.requests_per_day > 0 && this.usage.requests_today >= limits.requests_per_day) { return { allowed: false, reason: 'Daily request limit exceeded', waitTime: this.getTimeUntilDailyReset(), }; } if (limits.tokens_per_day > 0 && this.usage.tokens_today + estimatedTokens > limits.tokens_per_day) { return { allowed: false, reason: 'Daily token limit would be exceeded', waitTime: this.getTimeUntilDailyReset(), }; } if (this.usage.requests_this_minute >= limits.requests_per_minute) { return { allowed: false, reason: 'Per-minute request limit exceeded', waitTime: this.getTimeUntilMinuteReset(), }; } if (this.usage.tokens_this_minute + estimatedTokens > limits.tokens_per_minute) { return { allowed: false, reason: 'Per-minute token limit would be exceeded', waitTime: this.getTimeUntilMinuteReset(), }; } return { allowed: true }; } getUsageStats() { this.resetCountersIfNeeded(); const limits = this.isPaidTier ? this.PAID_TIER_LIMITS : this.FREE_TIER_LIMITS; return { tier: this.isPaidTier ? 'paid' : 'free', requests: { today: this.usage.requests_today, this_minute: this.usage.requests_this_minute, daily_limit: limits.requests_per_day, minute_limit: limits.requests_per_minute, daily_remaining: limits.requests_per_day < 0 ? -1 : Math.max(0, limits.requests_per_day - this.usage.requests_today), minute_remaining: Math.max(0, limits.requests_per_minute - this.usage.requests_this_minute), }, tokens: { today: this.usage.tokens_today, this_minute: this.usage.tokens_this_minute, daily_limit: limits.tokens_per_day, minute_limit: limits.tokens_per_minute, daily_remaining: limits.tokens_per_day < 0 ? -1 : Math.max(0, limits.tokens_per_day - this.usage.tokens_today), minute_remaining: Math.max(0, limits.tokens_per_minute - this.usage.tokens_this_minute), }, reset_times: { daily_reset_in: this.getTimeUntilDailyReset(), minute_reset_in: this.getTimeUntilMinuteReset(), }, }; } getQuotaStatus() { this.resetCountersIfNeeded(); const limits = this.isPaidTier ? this.PAID_TIER_LIMITS : this.FREE_TIER_LIMITS; const requests_daily_percent = limits.requests_per_day < 0 ? 0 : (this.usage.requests_today / limits.requests_per_day) * 100; const requests_minute_percent = (this.usage.requests_this_minute / limits.requests_per_minute) * 100; const tokens_daily_percent = limits.tokens_per_day < 0 ? 0 : (this.usage.tokens_today / limits.tokens_per_day) * 100; const tokens_minute_percent = (this.usage.tokens_this_minute / limits.tokens_per_minute) * 100; let overall_status = 'healthy'; if (requests_daily_percent >= 90 || tokens_daily_percent >= 90 || requests_minute_percent >= 95 || tokens_minute_percent >= 95) { overall_status = 'critical'; } else if (requests_daily_percent >= 80 || tokens_daily_percent >= 80 || requests_minute_percent >= 90 || tokens_minute_percent >= 90) { overall_status = 'warning'; } return { requests_daily_percent, requests_minute_percent, tokens_daily_percent, tokens_minute_percent, overall_status, }; } resetCountersIfNeeded() { const now = Date.now(); if (now - this.usage.last_reset_daily >= 24 * 60 * 60 * 1000) { this.usage.requests_today = 0; this.usage.tokens_today = 0; this.usage.last_reset_daily = now; logger.info('Daily quota counters reset'); } if (now - this.usage.last_reset_minute >= 60 * 1000) { this.usage.requests_this_minute = 0; this.usage.tokens_this_minute = 0; this.usage.last_reset_minute = now; logger.debug('Minute quota counters reset'); } } checkAndWarnLimits() { const status = this.getQuotaStatus(); const limits = this.isPaidTier ? this.PAID_TIER_LIMITS : this.FREE_TIER_LIMITS; if (status.requests_daily_percent >= 90) { logger.warn('⚠️ API quota critical: 90%+ of daily requests used', { used: this.usage.requests_today, limit: limits.requests_per_day, remaining: limits.requests_per_day - this.usage.requests_today, resetIn: this.formatDuration(this.getTimeUntilDailyReset()), }); } else if (status.requests_daily_percent >= 80) { logger.warn('📊 API quota warning: 80%+ of daily requests used', { used: this.usage.requests_today, limit: limits.requests_per_day, percent: Math.round(status.requests_daily_percent), }); } if (status.tokens_daily_percent >= 90) { logger.warn('⚠️ Token quota critical: 90%+ of daily tokens used', { used: this.usage.tokens_today, limit: limits.tokens_per_day, remaining: limits.tokens_per_day - this.usage.tokens_today, resetIn: this.formatDuration(this.getTimeUntilDailyReset()), }); } else if (status.tokens_daily_percent >= 80) { logger.warn('📊 Token quota warning: 80%+ of daily tokens used', { used: this.usage.tokens_today, limit: limits.tokens_per_day, percent: Math.round(status.tokens_daily_percent), }); } if (status.requests_minute_percent >= 95) { logger.warn('🚨 Rate limit critical: 95%+ of per-minute requests used', { used: this.usage.requests_this_minute, limit: limits.requests_per_minute, waitTime: this.formatDuration(this.getTimeUntilMinuteReset()), }); } } getTimeUntilDailyReset() { const nextReset = this.usage.last_reset_daily + (24 * 60 * 60 * 1000); return Math.max(0, nextReset - Date.now()); } getTimeUntilMinuteReset() { const nextReset = this.usage.last_reset_minute + (60 * 1000); return Math.max(0, nextReset - Date.now()); } formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } loadStoredUsage() { logger.debug('Quota monitor initialized', { tier: this.isPaidTier ? 'paid' : 'free', limits: this.isPaidTier ? this.PAID_TIER_LIMITS : this.FREE_TIER_LIMITS, }); } saveUsage() { } setupPeriodicReset() { const interval = setInterval(() => { this.resetCountersIfNeeded(); }, 30000); interval.unref(); } static estimateTokens(text, files = 0) { const textTokens = Math.ceil(text.length / 4); const fileTokens = files * 1000; return textTokens + fileTokens; } } let globalQuotaMonitor = null; export function getQuotaMonitor(isPaidTier) { if (!globalQuotaMonitor) { globalQuotaMonitor = new QuotaMonitor(isPaidTier); } return globalQuotaMonitor; } export function resetQuotaMonitor() { globalQuotaMonitor = null; }