api-model-limiter
Advanced tools
Comparing version 1.0.2 to 1.0.3
498
index.js
import Redis from 'ioredis'; | ||
import Validator from 'fastest-validator'; | ||
const v = new Validator(); | ||
const configSchema = { | ||
$$root: true, | ||
type: 'array', | ||
items: { | ||
type: 'object', | ||
props: { | ||
name: 'string', | ||
keys: { | ||
type: 'array', | ||
items: 'string', | ||
}, | ||
models: { | ||
type: 'array', | ||
items: 'array', | ||
items: { | ||
type: 'object', | ||
props: { | ||
name: 'string', | ||
limits: { | ||
type: 'object', | ||
minProps: 1, | ||
props: { | ||
minute: 'number|optional', | ||
day: 'number|optional', | ||
month: 'number|optional', | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
const optsSchema = { | ||
$$root: true, | ||
type: 'object', | ||
optional: true, | ||
default: {}, | ||
props: { | ||
customWindows: 'object|optional', | ||
keyPrefix: 'string|optional', | ||
redis: 'object|optional', | ||
modelStrategy: { | ||
type: 'string', | ||
optional: true, | ||
enum: ['ordered', 'random'], | ||
}, | ||
}, | ||
}; | ||
const freezeOptsSchema = { | ||
apiName: 'string', | ||
key: 'string', | ||
model: 'string', | ||
duration: 'number', | ||
}; | ||
class RateLimiter { | ||
constructor(config, options = {}) { | ||
config = arrify(config); | ||
// validate schema | ||
validate(configSchema, config); | ||
validate(optsSchema, options); | ||
this.redis = new Redis(options.redis); | ||
this.apis = config; | ||
this.defaultBatchSize = options.batchSize || 3; | ||
this.metricsEnabled = options.enableMetrics !== false; | ||
this.apis = arrify(config); | ||
this.customWindows = { | ||
@@ -17,12 +81,11 @@ minute: 60, | ||
this.keyPrefix = options.keyPrefix || 'ModelLimiter'; | ||
// Selection strategies | ||
this.keyStrategy = options.keyStrategy || 'ascending'; | ||
this.modelStrategy = options.modelStrategy || 'ascending'; | ||
this.keyStrategy = options.keyStrategy || 'ordered'; | ||
this.modelStrategy = options.modelStrategy || 'ordered'; | ||
// For round-robin tracking | ||
this.lastKeyIndices = {}; | ||
this.lastModelIndices = {}; | ||
// Validate strategies | ||
const validStrategies = ['ordered', 'random']; | ||
// Validate strategies | ||
const validStrategies = ['ascending', 'random', 'round-robin']; | ||
if (!validStrategies.includes(this.keyStrategy)) { | ||
@@ -36,360 +99,135 @@ throw new Error(`Invalid key strategy: ${this.keyStrategy}`); | ||
/** Quit */ | ||
quit() { | ||
this.redis.quit(); | ||
} | ||
/** | ||
* Changes the key selection strategy | ||
* Gets next available key:model combination with optional borrowing | ||
*/ | ||
setKeyStrategy(strategy) { | ||
const validStrategies = ['ascending', 'random', 'round-robin']; | ||
if (!validStrategies.includes(strategy)) { | ||
throw new Error(`Invalid key strategy: ${strategy}`); | ||
} | ||
this.keyStrategy = strategy; | ||
} | ||
async getModel(apiName) { | ||
// get api | ||
let api = this.apis.filter((o) => o.name == apiName)[0]; | ||
/** | ||
* Changes the model selection strategy | ||
*/ | ||
setModelStrategy(strategy) { | ||
const validStrategies = ['ascending', 'random', 'round-robin']; | ||
if (!validStrategies.includes(strategy)) { | ||
throw new Error(`Invalid model strategy: ${strategy}`); | ||
if (!api) { | ||
return { | ||
key: null, | ||
model: null, | ||
}; | ||
} | ||
this.modelStrategy = strategy; | ||
} | ||
/** | ||
* Gets the next round-robin index | ||
*/ | ||
getNextRoundRobinIndex(currentIndex, length) { | ||
return (currentIndex + 1) % length; | ||
} | ||
let redisKey, modelName, apiKey, validLimits, limitCount, resp; | ||
let limits = {}; | ||
let { keys, models } = api; | ||
/** | ||
* Gets a random index | ||
*/ | ||
getRandomIndex(length) { | ||
return Math.floor(Math.random() * length); | ||
} | ||
/** | ||
* Gets ordered items based on strategy | ||
*/ | ||
getOrderedItems(items, apiName, type) { | ||
const strategy = type === 'key' ? this.keyStrategy : this.modelStrategy; | ||
const indices = | ||
type === 'key' ? this.lastKeyIndices : this.lastModelIndices; | ||
switch (strategy) { | ||
case 'ascending': | ||
return [...items]; | ||
case 'random': | ||
const shuffled = [...items]; | ||
for (let i = shuffled.length - 1; i > 0; i--) { | ||
const j = Math.floor(Math.random() * (i + 1)); | ||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; | ||
} | ||
return shuffled; | ||
case 'round-robin': | ||
const key = `${apiName}-${type}`; | ||
if (!(key in indices)) { | ||
indices[key] = -1; | ||
} | ||
indices[key] = this.getNextRoundRobinIndex(indices[key], items.length); | ||
const ordered = [...items]; | ||
// Reorder array starting from last used index | ||
return [ | ||
...ordered.slice(indices[key]), | ||
...ordered.slice(0, indices[key]), | ||
]; | ||
default: | ||
return [...items]; | ||
if (this.modelStrategy == 'random') { | ||
models = arrayRandom(models); | ||
} | ||
} | ||
/** | ||
* Gets next available key:model combination with optional borrowing | ||
*/ | ||
async getModel(apiName, allowBorrowing = false) { | ||
const api = this.findApi(apiName); | ||
// Get ordered models and keys based on their respective strategies | ||
const orderedModels = this.getOrderedItems(api.models, apiName, 'model'); | ||
const orderedKeys = this.getOrderedItems(api.keys, apiName, 'key'); | ||
for (const model of orderedModels) { | ||
for (const key of orderedKeys) { | ||
const result = await this.checkAndIncrement(key, model, allowBorrowing); | ||
if (result.isWithinLimits) { | ||
return { | ||
key, | ||
model: model.name, | ||
limits: result.usage, | ||
borrowed: result.borrowed, | ||
}; | ||
} | ||
} | ||
if (this.keyStrategy == 'random') { | ||
keys = arrayRandom(keys); | ||
} | ||
return null; | ||
} | ||
/** | ||
* Gets multiple available key:model combinations at once | ||
*/ | ||
async getBatch(apiName, size = this.defaultBatchSize) { | ||
const api = this.findApi(apiName); | ||
const results = []; | ||
// loop thru models | ||
for (let { name: model, limits: modelLimits } of models) { | ||
// loop thru all keys | ||
for (let key of keys) { | ||
apiKey = key; | ||
// Get ordered models and keys based on their respective strategies | ||
const orderedModels = this.getOrderedItems(api.models, apiName, 'model'); | ||
const orderedKeys = this.getOrderedItems(api.keys, apiName, 'key'); | ||
validLimits = true; | ||
for (const model of orderedModels) { | ||
for (const key of orderedKeys) { | ||
if (results.length >= size) break; | ||
let parsedModelLimits = Object.entries(modelLimits) | ||
.map(([limit, startCount]) => { | ||
return { limit, startCount, duration: this.customWindows[limit] }; | ||
}) | ||
.filter((o) => o.duration > 0); | ||
const result = await this.checkAndIncrement(key, model); | ||
if (result.isWithinLimits) { | ||
results.push({ | ||
key, | ||
model: model.name, | ||
limits: result.usage, | ||
borrowed: result.borrowed, | ||
}); | ||
} | ||
} | ||
} | ||
for (let { limit, startCount, duration } of parsedModelLimits) { | ||
redisKey = `${this.keyPrefix}:${apiName}:${key}:${model}:${limit}`; | ||
return results.length > 0 ? results : null; | ||
} | ||
// check if key exists and count is zero | ||
({ resp, limitCount } = await this.redis | ||
.get(redisKey) | ||
.then((resp) => { | ||
if (resp !== null) resp = Number(resp); | ||
return { limitCount: resp !== null ? resp : startCount, resp }; | ||
})); | ||
/** | ||
* Helper to find API configuration | ||
*/ | ||
findApi(apiName) { | ||
const api = this.apis.find((a) => a.name === apiName); | ||
if (!api) throw new Error(`API "${apiName}" not found`); | ||
return api; | ||
} | ||
// console.log({ limit, resp, limitCount }); | ||
/** | ||
* Creates a unique Redis key for a specific window | ||
*/ | ||
getKey(apiKey, modelName, window) { | ||
const timestamp = this.getWindowTimestamp(window); | ||
return `rate:${apiKey}:${modelName}:${window}:${timestamp}`; | ||
} | ||
limits[limit] = limitCount; | ||
/** | ||
* Gets the current timestamp for a specific window | ||
*/ | ||
getWindowTimestamp(window) { | ||
const now = Math.floor(Date.now() / 1000); | ||
const windowSize = this.customWindows[window]; | ||
if (!windowSize) throw new Error(`Invalid window: ${window}`); | ||
return now - (now % windowSize); | ||
} | ||
if (!limitCount) { | ||
validLimits = false; | ||
break; | ||
} | ||
/** | ||
* Gets remaining time in current window | ||
*/ | ||
getWindowExpiry(window) { | ||
const now = Math.floor(Date.now() / 1000); | ||
const windowStart = this.getWindowTimestamp(window); | ||
const windowSize = this.customWindows[window]; | ||
return windowStart + windowSize - now; | ||
} | ||
if (limitCount) { | ||
if (!resp && limit in this.customWindows) { | ||
await this.redis.setex(redisKey, duration, limitCount); | ||
} | ||
} | ||
} | ||
/** | ||
* Checks if a key:model combination is frozen | ||
*/ | ||
async isFrozen(apiKey, modelName) { | ||
const freezeKey = `freeze:${apiKey}:${modelName}`; | ||
const result = await this.redis.get(freezeKey); | ||
return result !== null; | ||
} | ||
/** | ||
* Checks and increments rate limits for a key:model combination | ||
*/ | ||
async checkAndIncrement(apiKey, model, allowBorrowing = false) { | ||
// Check if frozen | ||
if (await this.isFrozen(apiKey, model.name)) { | ||
return { isWithinLimits: false, usage: {} }; | ||
} | ||
const pipeline = this.redis.pipeline(); | ||
const limits = model.limits; | ||
const keys = {}; | ||
// Set up all keys and initialize if needed | ||
for (const [window, limit] of Object.entries(limits)) { | ||
const key = this.getKey(apiKey, model.name, window); | ||
const expiry = this.getWindowExpiry(window); | ||
keys[window] = { key, limit, expiry }; | ||
pipeline.incr(key); | ||
pipeline.expire(key, expiry); | ||
} | ||
// Execute all commands atomically | ||
const results = await pipeline.exec(); | ||
const usage = {}; | ||
let isWithinLimits = true; | ||
let borrowed = false; | ||
// Process results | ||
let i = 0; | ||
for (const [window, { key, limit, expiry }] of Object.entries(keys)) { | ||
const count = results[i * 2][1]; | ||
usage[window] = { | ||
used: count, | ||
remaining: Math.max(0, limit - count), | ||
limit, | ||
reset: expiry, | ||
}; | ||
if (count > limit) { | ||
if (allowBorrowing && window !== 'month') { | ||
borrowed = true; | ||
} else { | ||
isWithinLimits = false; | ||
} | ||
if (validLimits) break; | ||
} | ||
i++; | ||
} | ||
// If we went over limit and couldn't borrow, rollback | ||
if (!isWithinLimits) { | ||
const rollback = this.redis.pipeline(); | ||
for (const { key } of Object.values(keys)) { | ||
rollback.decr(key); | ||
// if valid, set model | ||
if (validLimits) { | ||
modelName = model; | ||
break; | ||
} | ||
await rollback.exec(); | ||
} | ||
return { isWithinLimits, usage, borrowed }; | ||
} | ||
// decrease model hits | ||
if (modelName) { | ||
let keyPat = `${this.keyPrefix}:${apiName}:${apiKey}:${modelName}:*`; | ||
let affectedKeys = await this.redis.keys(keyPat); | ||
/** | ||
* Freezes a key:model combination | ||
*/ | ||
async freezeModel(apiName, key, modelName, duration) { | ||
this.findApi(apiName); // Validate API exists | ||
const freezeKey = `freeze:${key}:${modelName}`; | ||
await this.redis.set(freezeKey, '1', 'EX', duration); | ||
} | ||
/** | ||
* Updates rate limits for a model | ||
*/ | ||
async updateLimits(apiName, modelName, newLimits) { | ||
const api = this.findApi(apiName); | ||
const model = api.models.find((m) => m.name === modelName); | ||
if (!model) | ||
throw new Error(`Model ${modelName} not found in API ${apiName}`); | ||
// Validate new limits | ||
for (const [window, limit] of Object.entries(newLimits)) { | ||
if (!this.customWindows[window]) | ||
throw new Error(`Invalid window: ${window}`); | ||
if (typeof limit !== 'number' || limit < 0) | ||
throw new Error(`Invalid limit for ${window}`); | ||
for (let redisKey of affectedKeys) { | ||
this.redis.decr(redisKey); | ||
} | ||
} | ||
model.limits = { ...model.limits, ...newLimits }; | ||
return model.limits; | ||
} | ||
/** | ||
* Gets the metric key | ||
*/ | ||
getMetricKey(apiKey, modelName, type) { | ||
const day = this.getWindowTimestamp('day'); | ||
return `metric:${apiKey}:${modelName}:${type}:${day}`; | ||
} | ||
/** | ||
* Updates metrics for rate limiting events | ||
*/ | ||
async updateMetrics(apiKey, modelName, type) { | ||
if (!this.metricsEnabled) return; | ||
const key = this.getMetricKey(apiKey, modelName, type); | ||
const expiry = this.getWindowExpiry('day'); | ||
await this.redis.pipeline().incr(key).expire(key, expiry).exec(); | ||
} | ||
/** | ||
* Gets usage metrics for a key:model combination | ||
*/ | ||
async getMetrics(apiName, apiKey, modelName) { | ||
this.findApi(apiName); // Validate API exists | ||
if (!this.metricsEnabled) return null; | ||
const types = ['success', 'limit_reached', 'borrowed']; | ||
const pipeline = this.redis.pipeline(); | ||
for (const type of types) { | ||
pipeline.get(this.getMetricKey(apiKey, modelName, type)); | ||
// console.log(modelKeys) | ||
if (!modelName) { | ||
return { | ||
model: modelName, | ||
key: apiKey, | ||
}; | ||
} | ||
const results = await pipeline.exec(); | ||
return { | ||
success: parseInt(results[0][1]) || 0, | ||
limitReached: parseInt(results[1][1]) || 0, | ||
borrowed: parseInt(results[2][1]) || 0, | ||
key: apiKey, | ||
model: modelName, | ||
limits: limits, | ||
}; | ||
} | ||
/** | ||
* Gets current usage statistics | ||
*/ | ||
async getUsageStats(apiName, apiKey, modelName) { | ||
const api = this.findApi(apiName); | ||
const model = api.models.find((m) => m.name === modelName); | ||
if (!model) | ||
throw new Error(`Model ${modelName} not found in API ${apiName}`); | ||
async freezeModel(options) { | ||
validate(freezeOptsSchema, options); | ||
let { apiName, key, model, duration } = options; | ||
const metrics = await this.getMetrics(apiName, apiKey, modelName); | ||
const pipeline = this.redis.pipeline(); | ||
const windows = Object.keys(this.customWindows); | ||
console.log({ apiName, key, model, duration }); | ||
let redisKeyPat = `${this.keyPrefix}:${apiName}:${key}:${model}:*`; | ||
let allKeys = await this.redis.keys(redisKeyPat); | ||
// Get current usage for all windows | ||
for (const window of windows) { | ||
const key = this.getKey(apiKey, modelName, window); | ||
pipeline.get(key); | ||
for (let redisKey of allKeys) { | ||
await this.redis.setex(redisKey, duration, 0); | ||
} | ||
} | ||
} | ||
const results = await pipeline.exec(); | ||
const usage = {}; | ||
function arrayRandom(array) { | ||
for (let i = array.length - 1; i > 0; i--) { | ||
const j = Math.floor(Math.random() * (i + 1)); | ||
[array[i], array[j]] = [array[j], array[i]]; | ||
} | ||
return array; | ||
} | ||
// Process usage for each window | ||
windows.forEach((window, i) => { | ||
const count = parseInt(results[i][1]) || 0; | ||
usage[window] = { | ||
used: count, | ||
remaining: model.limits[window] | ||
? Math.max(0, model.limits[window] - count) | ||
: null, | ||
limit: model.limits[window] || null, | ||
reset: this.getWindowExpiry(window), | ||
}; | ||
}); | ||
function arrify(v) { | ||
if (v === undefined) return []; | ||
return Array.isArray(v) ? v : [v]; | ||
} | ||
return { | ||
currentUsage: usage, | ||
metrics: this.metricsEnabled ? metrics : null, | ||
}; | ||
function validate(schema, obj) { | ||
const check = v.compile(schema); | ||
let isValid = check(obj); | ||
if (isValid !== true) { | ||
throw new Error(isValid[0].message); | ||
} | ||
@@ -396,0 +234,0 @@ } |
{ | ||
"name": "api-model-limiter", | ||
"version": "1.0.2", | ||
"version": "1.0.3", | ||
"main": "index.js", | ||
@@ -8,2 +8,3 @@ "type": "module", | ||
"dependencies": { | ||
"fastest-validator": "^1.19.0", | ||
"ioredis": "^5.4.1" | ||
@@ -10,0 +11,0 @@ }, |
346
README.md
@@ -8,5 +8,7 @@ # API-Model Limiter | ||
### Rate Limiting Windows | ||
Rate limits are tracked across different time windows (minute, hour, day, month). Each window operates independently, meaning a request must satisfy ALL window constraints to be allowed. | ||
Example: | ||
```javascript | ||
@@ -21,2 +23,3 @@ { | ||
### Limit Borrowing | ||
Limit borrowing allows exceeding shorter time window limits by "borrowing" from longer windows. This is useful for handling burst traffic while maintaining overall usage constraints. | ||
@@ -26,2 +29,3 @@ | ||
Consider an API with limits: | ||
- 10 requests/minute | ||
@@ -31,15 +35,20 @@ - 50 requests/hour | ||
Without borrowing: | ||
```javascript | ||
// If you've used 10 requests in the current minute | ||
result = await limiter.getModel("api 1", false); // Returns null | ||
result = await limiter.getModel('api 1'); | ||
``` | ||
With borrowing: | ||
```javascript | ||
// Even if minute limit (10) is reached, but hour limit has space | ||
result = await limiter.getModel("api 1", true); // Returns valid combination | ||
This returns: | ||
```json | ||
{ | ||
"key": "key 1", | ||
"model": "model 1", | ||
"limits": { "minute": 13 } | ||
} | ||
``` | ||
When we have exhausted all model limits, then the `model` property will be null. | ||
**When to Use Borrowing:** | ||
- Handling burst traffic (e.g., batch processing) | ||
@@ -51,5 +60,7 @@ - Managing irregular usage patterns | ||
### Key-Model Rotation | ||
The system automatically rotates through available API keys and models when limits are reached. This helps maximize availability and distribute load. | ||
**Use Case:** | ||
```javascript | ||
@@ -80,3 +91,4 @@ const config = [ | ||
1. **Ascending** (default) | ||
1. **Ordered** (default) | ||
- Uses keys/models in the order they are defined | ||
@@ -91,73 +103,17 @@ - Predictable, sequential access pattern | ||
3. **Round-Robin** | ||
- Rotates through keys/models sequentially | ||
- Ensures even distribution | ||
- Maintains selection state between calls | ||
### Configuration | ||
Set strategies during initialization: | ||
```javascript | ||
const limiter = new RateLimiter(config, { | ||
keyStrategy: 'round-robin', // Strategy for key selection | ||
modelStrategy: 'random', // Strategy for model selection | ||
redis: { /* redis options */ }, | ||
// ... other options | ||
keyStrategy: 'round-robin', // Strategy for key selection | ||
modelStrategy: 'random', // Strategy for model selection | ||
redis: { | ||
/* redis options */ | ||
}, | ||
// ... other options | ||
}); | ||
``` | ||
Change strategies at runtime: | ||
```javascript | ||
// Change key selection strategy | ||
limiter.setKeyStrategy('random'); | ||
// Change model selection strategy | ||
limiter.setModelStrategy('round-robin'); | ||
``` | ||
### Use Cases | ||
1. **Ascending Strategy** | ||
```javascript | ||
// Keys/models used in defined order | ||
const limiter = new RateLimiter(config, { | ||
keyStrategy: 'ascending', | ||
modelStrategy: 'ascending' | ||
}); | ||
``` | ||
Best for: | ||
- Prioritized keys/models (most important first) | ||
- Simple, predictable behavior | ||
- Sequential access patterns | ||
2. **Random Strategy** | ||
```javascript | ||
const limiter = new RateLimiter(config, { | ||
keyStrategy: 'random', | ||
modelStrategy: 'random' | ||
}); | ||
``` | ||
Best for: | ||
- Load balancing across keys | ||
- Avoiding predictable patterns | ||
- Distributing usage randomly | ||
3. **Round-Robin Strategy** | ||
```javascript | ||
const limiter = new RateLimiter(config, { | ||
keyStrategy: 'round-robin', | ||
modelStrategy: 'round-robin' | ||
}); | ||
``` | ||
Best for: | ||
- Even distribution of usage | ||
- Fair allocation of resources | ||
- Predictable rotation patterns | ||
## Installation | ||
```bash | ||
npm install ioredis | ||
``` | ||
## Configuration Options | ||
@@ -169,20 +125,19 @@ | ||
const limiter = new RateLimiter(config, { | ||
redis: { | ||
host: 'localhost', | ||
port: 6379, | ||
password: 'optional', | ||
db: 0, | ||
maxRetriesPerRequest: 3, | ||
retryStrategy: (times) => Math.min(times * 50, 2000), | ||
enableOfflineQueue: true, | ||
connectTimeout: 10000 | ||
}, | ||
enableMetrics: true, | ||
batchSize: 5, | ||
customWindows: { | ||
shift: 28800, | ||
week: 604800 | ||
}, | ||
keyStrategy: 'ascending', | ||
modelStrategy: 'round-robin' | ||
redis: { | ||
host: 'localhost', | ||
port: 6379, | ||
password: 'optional', | ||
db: 0, | ||
maxRetriesPerRequest: 3, | ||
retryStrategy: (times) => Math.min(times * 50, 2000), | ||
enableOfflineQueue: true, | ||
connectTimeout: 10000, | ||
}, | ||
customWindows: { | ||
shift: 28800, | ||
week: 604800, | ||
}, | ||
keyStrategy: 'ordered', | ||
modelStrategy: 'random', | ||
}); | ||
@@ -197,70 +152,27 @@ ``` | ||
const config = [ | ||
{ | ||
name: "api 1", // Unique API identifier | ||
keys: ["key1", "key2"], // Array of API keys | ||
models: [ // Array of models | ||
{ | ||
name: "model1", // Unique model identifier | ||
limits: { // Rate limits per window | ||
minute: 44, | ||
hour: 442, | ||
day: 2000, | ||
month: 200000 | ||
} | ||
} | ||
] | ||
} | ||
// ... more APIs | ||
{ | ||
name: 'api 1', // Unique API identifier | ||
keys: ['key1', 'key2'], // Array of API keys | ||
models: [ | ||
// Array of models | ||
{ | ||
name: 'model1', // Unique model identifier | ||
limits: { | ||
// Rate limits per window | ||
minute: 44, | ||
hour: 442, | ||
day: 2000, | ||
month: 200000, | ||
}, | ||
}, | ||
], | ||
}, | ||
// ... more APIs | ||
]; | ||
``` | ||
### Redis Options | ||
Control your Redis connection settings: | ||
### Custom Windows Options | ||
```javascript | ||
{ | ||
redis: { | ||
// Connection | ||
host: 'localhost', // Redis host (default: 'localhost') | ||
port: 6379, // Redis port (default: 6379) | ||
password: 'secret', // Redis password (optional) | ||
db: 0, // Redis database number (default: 0) | ||
// Timeouts | ||
connectTimeout: 10000, // Connection timeout in ms (default: 10000) | ||
commandTimeout: 5000, // Command execution timeout (default: 5000) | ||
// Retry Configuration | ||
maxRetriesPerRequest: 3, // Max retries per command (default: 3) | ||
retryStrategy: (times) => { // Custom retry strategy | ||
return Math.min(times * 50, 2000); | ||
}, | ||
// Advanced Options | ||
enableOfflineQueue: true, // Queue commands when disconnected (default: true) | ||
keepAlive: 30000, // TCP keep-alive in ms (default: 30000) | ||
enableAutoPipelining: true, // Enable auto pipelining (default: true) | ||
// TLS Options (if needed) | ||
tls: { | ||
// TLS configuration options | ||
ca: fs.readFileSync('path/to/ca.crt'), | ||
cert: fs.readFileSync('path/to/client.crt'), | ||
key: fs.readFileSync('path/to/client.key') | ||
} | ||
} | ||
} | ||
``` | ||
### Metrics Options | ||
```javascript | ||
{ | ||
enableMetrics: true, // Enable/disable metrics collection (default: true) | ||
metricsPrefix: 'custom', // Custom prefix for metric keys (default: 'metric') | ||
} | ||
``` | ||
### Custom Windows Options | ||
```javascript | ||
{ | ||
customWindows: { | ||
@@ -279,3 +191,4 @@ // Window name: duration in seconds | ||
### getModel(apiName, allowBorrowing) | ||
### getModel(apiName) | ||
Get next available key:model combination for a specific API. | ||
@@ -285,25 +198,12 @@ | ||
// Get next available combination for "api 1" | ||
const result = await limiter.getModel("api 1", false); | ||
const result = await limiter.getModel('api 1'); | ||
if (result) { | ||
console.log('Key:', result.key); | ||
console.log('Model:', result.model); | ||
console.log('Limits:', result.limits); | ||
console.log('Key:', result.key); | ||
console.log('Model:', result.model); | ||
console.log('Limits:', result.limits); | ||
} | ||
``` | ||
### getBatch(apiName, size) | ||
Get multiple combinations for a specific API at once. | ||
### freezeModel({apiName, key, model, duration}) | ||
```javascript | ||
// Get 3 combinations for "api 1" | ||
const batch = await limiter.getBatch("api 1", 3); | ||
if (batch) { | ||
batch.forEach(result => { | ||
console.log('Key:', result.key); | ||
console.log('Model:', result.model); | ||
}); | ||
} | ||
``` | ||
### freezeModel(apiName, key, modelName, duration) | ||
Freeze a specific key:model combination for an API. | ||
@@ -313,106 +213,12 @@ | ||
// Freeze for 5 minutes | ||
await limiter.freezeModel("api 1", "key1", "model1", 300); | ||
``` | ||
### updateLimits(apiName, modelName, newLimits) | ||
Update limits for a specific model in an API. | ||
```javascript | ||
const newLimits = await limiter.updateLimits("api 1", "model1", { | ||
minute: 50, | ||
hour: 500 | ||
await limiter.freezeModel({ | ||
apiName: 'api 1', | ||
key: 'key 1', | ||
model: 'model 1', | ||
duration: 3000, | ||
}); | ||
``` | ||
### getUsageStats(apiName, apiKey, modelName) | ||
Get usage statistics for a specific key:model combination. | ||
```javascript | ||
const stats = await limiter.getUsageStats("api 1", "key1", "model1"); | ||
console.log('Current usage:', stats.currentUsage); | ||
console.log('Metrics:', stats.metrics); | ||
``` | ||
### getMetrics(apiName, apiKey, modelName) | ||
Get metrics for a specific key:model combination. | ||
```javascript | ||
const metrics = await limiter.getMetrics("api 1", "key1", "model1"); | ||
console.log('Success rate:', metrics.success); | ||
console.log('Limit reached count:', metrics.limitReached); | ||
``` | ||
### Selection Strategy Methods | ||
#### setKeyStrategy(strategy) | ||
Change the key selection strategy. | ||
```javascript | ||
limiter.setKeyStrategy('random'); // 'ascending', 'random', or 'round-robin' | ||
``` | ||
#### setModelStrategy(strategy) | ||
Change the model selection strategy. | ||
```javascript | ||
limiter.setModelStrategy('round-robin'); // 'ascending', 'random', or 'round-robin' | ||
``` | ||
## Error Handling | ||
The rate limiter includes comprehensive error handling: | ||
```javascript | ||
try { | ||
const result = await limiter.getModel("non-existent-api"); | ||
} catch (error) { | ||
console.error('API not found:', error.message); | ||
} | ||
try { | ||
limiter.setKeyStrategy('invalid-strategy'); | ||
} catch (error) { | ||
console.error('Invalid strategy:', error.message); | ||
} | ||
``` | ||
## Best Practices and Recommendations | ||
1. **Limit Borrowing Strategy** | ||
- Enable for premium/priority operations | ||
- Use with caution on public APIs | ||
- Consider implementing graduated borrowing limits | ||
2. **Key-Model Rotation** | ||
- Distribute keys across different services/regions | ||
- Implement fallback models with different capabilities | ||
- Monitor rotation patterns for optimization | ||
3. **Metrics Usage** | ||
- Set up alerts for high limit-reached rates | ||
- Monitor borrowed limits for capacity planning | ||
- Track usage patterns for optimization | ||
4. **Error Handling** | ||
- Implement exponential backoff with freezing | ||
- Log all limit-reached events | ||
- Set up monitoring for frozen combinations | ||
5. **Selection Strategy Best Practices** | ||
- Use 'ascending' for predictable, prioritized access | ||
- Use 'random' for basic load balancing | ||
- Use 'round-robin' for fair resource distribution | ||
- Monitor distribution patterns with metrics | ||
- Consider changing strategies based on load patterns | ||
6. **Strategy Selection Guidelines** | ||
- Keys: | ||
- Use 'round-robin' when all keys have equal priority | ||
- Use 'ascending' when keys have different quotas/costs | ||
- Use 'random' for unpredictable load distribution | ||
- Models: | ||
- Use 'round-robin' for balanced model usage | ||
- Use 'ascending' for fallback patterns | ||
- Use 'random' for A/B testing or load balancing | ||
## License | ||
MIT | ||
MIT |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
11697
2
198
213
1
+ Addedfastest-validator@^1.19.0
+ Addedfastest-validator@1.19.0(transitive)