Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

api-model-limiter

Package Overview
Dependencies
Maintainers
0
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

api-model-limiter - npm Package Compare versions

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 @@ },

@@ -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
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc