rate-limiter.js•7.56 kB
/**
* Type guard for Axios errors
*/
function isAxiosError(error) {
return error && error.isAxiosError === true;
}
import { MODELS } from '../config/models.js';
import { RATE_LIMITS, ERROR_MESSAGES, API } from '../config/constants.js';
/**
* Manages rate limiting for Gemini API requests
* Tracks request timestamps, implements exponential backoff,
* and retries failed requests when appropriate
*/
export class RateLimitManager {
/**
* Creates a new RateLimitManager
* @param isPaidTier Whether the user is on paid tier
*/
constructor(isPaidTier = false) {
/** Map to track request timestamps by model ID */
this.requestTimestamps = new Map();
this.isPaidTier = isPaidTier;
}
/**
* Checks if a request can be made for the specified model
* @param modelId The model ID to check
* @returns true if a request can be made, false otherwise
*/
canMakeRequest(modelId) {
const model = MODELS[modelId];
if (!model) {
console.error(`[RateLimitManager] Unknown model: ${modelId}`);
return false; // Unknown model, cannot make request
}
// Get the rate limit for this model
const rpm = model.freeRpm; // For now, use free tier RPM
// Initialize tracking for this model if it doesn't exist
if (!this.requestTimestamps.has(modelId)) {
this.requestTimestamps.set(modelId, []);
console.error(`[RateLimitManager] Initialized timestamps for ${modelId}.`);
return true;
}
// Get timestamps and filter to those in the last minute
const timestamps = this.requestTimestamps.get(modelId);
const now = Date.now();
const oneMinuteAgo = now - 60 * 1000;
// Prune timestamps older than 1 minute
const recentTimestamps = timestamps.filter(t => t >= oneMinuteAgo);
this.requestTimestamps.set(modelId, recentTimestamps);
console.error(`[RateLimitManager] Checking canMakeRequest for ${modelId}: recentTimestamps=${JSON.stringify(recentTimestamps)}, count=${recentTimestamps.length}, rpm=${rpm}`);
// Check if we're under the rate limit
return recentTimestamps.length < rpm;
}
/**
* Records a request for the specified model
* @param modelId The model ID to record a request for
*/
recordRequest(modelId) {
if (!this.requestTimestamps.has(modelId)) {
this.requestTimestamps.set(modelId, []);
}
const timestamps = this.requestTimestamps.get(modelId);
timestamps.push(Date.now());
// Prune old timestamps immediately after adding
const now = Date.now();
const oneMinuteAgo = now - 60 * 1000;
const prunedTimestamps = timestamps.filter(t => t >= oneMinuteAgo);
this.requestTimestamps.set(modelId, prunedTimestamps);
console.error(`[RateLimitManager] Recorded request for ${modelId}: prunedTimestamps=${JSON.stringify(prunedTimestamps)}, count=${prunedTimestamps.length}`);
}
/**
* Calculates time until next request slot is available
* @param modelId The model ID to check
* @returns Time in milliseconds until next available request slot
*/
getTimeUntilNextSlot(modelId) {
if (this.canMakeRequest(modelId)) {
return 0; // Can make request now
}
const model = MODELS[modelId];
if (!model) {
return 60000; // Unknown model, wait a minute
}
const rpm = model.freeRpm;
const timestamps = this.requestTimestamps.get(modelId);
if (timestamps.length < rpm) {
return 0; // Not at limit yet
}
// Sort timestamps to find the oldest one
timestamps.sort((a, b) => a - b);
// When will the oldest timestamp expire?
const oldestTimestamp = timestamps[timestamps.length - rpm];
const expiryTime = oldestTimestamp + 60000; // 1 minute after the request
const now = Date.now();
return Math.max(0, expiryTime - now);
}
/**
* Executes an API call with automatic retry for rate limiting
* @param modelId The model ID being used
* @param apiCall Function that makes the API call
* @returns Promise that resolves to the API response
*/
async executeWithRetry(modelId, apiCall) {
let retries = 0;
while (retries <= RATE_LIMITS.MAX_RETRIES) {
try {
// Check if we can make a request
if (!this.canMakeRequest(modelId) && retries === 0) {
// If this is our first attempt and we're rate limited,
// calculate wait time and throw error
const waitTime = this.getTimeUntilNextSlot(modelId);
const waitTimeSeconds = Math.ceil(waitTime / 1000);
throw new Error(ERROR_MESSAGES.RATE_LIMIT(modelId, waitTimeSeconds));
}
// Make the API call
const response = await apiCall();
// Record the successful request
this.recordRequest(modelId);
return response;
}
catch (error) {
// Check if it's a rate limit error from the API
if (this.isRateLimitError(error) && retries < RATE_LIMITS.MAX_RETRIES) {
// Calculate backoff time with exponential backoff + jitter
const backoffTime = this.calculateBackoff(retries);
// Log the rate limit issue
console.error(`Rate limit hit for ${modelId}. Retrying in ${backoffTime}ms...`);
// Wait for the backoff period
await this.sleep(backoffTime);
// Increment retry counter
retries++;
}
else {
// Not a rate limit error or we've reached max retries
throw error;
}
}
}
// Should not reach here due to throw in the while loop,
// but TypeScript requires a return statement
throw new Error(`Maximum retries (${RATE_LIMITS.MAX_RETRIES}) reached for ${modelId}`);
}
/**
* Checks if an error is a rate limit error
* @param error The error to check
* @returns true if it's a rate limit error, false otherwise
*/
isRateLimitError(error) {
return isAxiosError(error) && error.response?.status === 429;
}
/**
* Calculates backoff time with exponential backoff + jitter
* @param retry Current retry count
* @returns Backoff time in milliseconds
*/
calculateBackoff(retry) {
// Calculate exponential backoff
const exponentialDelay = Math.min(RATE_LIMITS.MAX_DELAY_MS, RATE_LIMITS.BASE_DELAY_MS * Math.pow(2, retry));
// Add jitter
const jitter = RATE_LIMITS.JITTER_FACTOR;
const randomFactor = 1 - jitter + (Math.random() * jitter * 2);
return Math.floor(exponentialDelay * randomFactor);
}
/**
* Sleep for a specified duration
* @param ms Time to sleep in milliseconds
* @returns Promise that resolves after the sleep duration
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
/**
* Create and export a singleton instance of the rate limit manager
*/
export const rateLimitManager = new RateLimitManager(API.IS_PAID_TIER);
//# sourceMappingURL=rate-limiter.js.map