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
JavaScript
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;
}