ai-cost-controls 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/cjs/cost-controls.js +167 -0
- package/dist/cjs/cost-controls.js.map +1 -0
- package/dist/cjs/in-memory-backend.js +48 -0
- package/dist/cjs/in-memory-backend.js.map +1 -0
- package/dist/cjs/index.js +10 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/interfaces.js +11 -0
- package/dist/cjs/interfaces.js.map +1 -0
- package/dist/esm/cost-controls.js +163 -0
- package/dist/esm/cost-controls.js.map +1 -0
- package/dist/esm/in-memory-backend.js +44 -0
- package/dist/esm/in-memory-backend.js.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/interfaces.js +8 -0
- package/dist/esm/interfaces.js.map +1 -0
- package/dist/types/cost-controls.d.ts +42 -0
- package/dist/types/cost-controls.d.ts.map +1 -0
- package/dist/types/in-memory-backend.d.ts +18 -0
- package/dist/types/in-memory-backend.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/interfaces.d.ts +34 -0
- package/dist/types/interfaces.d.ts.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ray Ockenfels
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# ai-cost-controls
|
|
2
|
+
|
|
3
|
+
Framework-agnostic AI cost controls: per-user rate limiting, token budget tracking, and response caching with pluggable cache backends.
|
|
4
|
+
|
|
5
|
+
**Zero runtime dependencies.** Bring your own cache backend (ioredis, @upstash/redis, Cloudflare KV, etc.) or use the built-in in-memory backend.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install ai-cost-controls
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { CostControls } from 'ai-cost-controls';
|
|
17
|
+
|
|
18
|
+
const controls = new CostControls({
|
|
19
|
+
config: {
|
|
20
|
+
rateLimitPerMinute: 20,
|
|
21
|
+
dailyTokenBudget: 100_000,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Check rate limit before making an AI call
|
|
26
|
+
if (!(await controls.checkRateLimit(userId))) {
|
|
27
|
+
throw new Error('Rate limited');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check cache first
|
|
31
|
+
const cached = await controls.getCachedResponse(userId, userMessage);
|
|
32
|
+
if (cached) return cached;
|
|
33
|
+
|
|
34
|
+
// After getting AI response, cache it and track tokens
|
|
35
|
+
await controls.cacheResponse(userId, userMessage, aiResponse);
|
|
36
|
+
await controls.trackTokenUsage(userId, inputTokens, outputTokens);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
| Option | Default | Description |
|
|
42
|
+
|--------|---------|-------------|
|
|
43
|
+
| `rateLimitPerMinute` | `20` | Max requests per user per minute |
|
|
44
|
+
| `cacheTtlSeconds` | `300` | Response cache TTL (5 minutes) |
|
|
45
|
+
| `dailyTokenBudget` | `100,000` | Max tokens per user per day |
|
|
46
|
+
| `monthlyTokenBudget` | `2,000,000` | Max tokens per user per month |
|
|
47
|
+
|
|
48
|
+
### Static Config
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const controls = new CostControls({
|
|
52
|
+
config: {
|
|
53
|
+
rateLimitPerMinute: 10,
|
|
54
|
+
dailyTokenBudget: 50_000,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Dynamic Config (e.g., from database)
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const controls = new CostControls({
|
|
63
|
+
configLoader: async () => {
|
|
64
|
+
const row = await db.query('SELECT * FROM ai_config LIMIT 1');
|
|
65
|
+
return {
|
|
66
|
+
rateLimitPerMinute: row.rate_limit,
|
|
67
|
+
dailyTokenBudget: row.daily_budget,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Cache Backends
|
|
74
|
+
|
|
75
|
+
The package ships with `InMemoryCacheBackend` (single-process only). For production multi-process or serverless deployments, implement the `CacheBackend` interface with your preferred client.
|
|
76
|
+
|
|
77
|
+
### Interface
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
interface CacheBackend {
|
|
81
|
+
get(key: string): Promise<string | null>;
|
|
82
|
+
set(key: string, value: string, ttlMs: number): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### ioredis
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import Redis from 'ioredis';
|
|
90
|
+
import { CostControls, CacheBackend } from 'ai-cost-controls';
|
|
91
|
+
|
|
92
|
+
const redis = new Redis();
|
|
93
|
+
|
|
94
|
+
const redisBackend: CacheBackend = {
|
|
95
|
+
async get(key) {
|
|
96
|
+
return redis.get(key);
|
|
97
|
+
},
|
|
98
|
+
async set(key, value, ttlMs) {
|
|
99
|
+
await redis.set(key, value, 'PX', ttlMs);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const controls = new CostControls({ cacheBackend: redisBackend });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### @upstash/redis
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { Redis } from '@upstash/redis';
|
|
110
|
+
import { CostControls, CacheBackend } from 'ai-cost-controls';
|
|
111
|
+
|
|
112
|
+
const redis = new Redis({ url: '...', token: '...' });
|
|
113
|
+
|
|
114
|
+
const upstashBackend: CacheBackend = {
|
|
115
|
+
async get(key) {
|
|
116
|
+
return redis.get<string>(key);
|
|
117
|
+
},
|
|
118
|
+
async set(key, value, ttlMs) {
|
|
119
|
+
await redis.set(key, value, { px: ttlMs });
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const controls = new CostControls({ cacheBackend: upstashBackend });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Cloudflare KV
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { CacheBackend } from 'ai-cost-controls';
|
|
130
|
+
|
|
131
|
+
// In a Cloudflare Worker
|
|
132
|
+
const kvBackend: CacheBackend = {
|
|
133
|
+
async get(key) {
|
|
134
|
+
return env.AI_CACHE.get(key);
|
|
135
|
+
},
|
|
136
|
+
async set(key, value, ttlMs) {
|
|
137
|
+
await env.AI_CACHE.put(key, value, { expirationTtl: Math.ceil(ttlMs / 1000) });
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Framework Integration
|
|
143
|
+
|
|
144
|
+
### Vercel AI SDK
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { streamText } from 'ai';
|
|
148
|
+
import { CostControls } from 'ai-cost-controls';
|
|
149
|
+
|
|
150
|
+
const controls = new CostControls();
|
|
151
|
+
|
|
152
|
+
async function chat(userId: string, message: string) {
|
|
153
|
+
if (!(await controls.checkRateLimit(userId))) {
|
|
154
|
+
return new Response('Rate limited', { status: 429 });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const cached = await controls.getCachedResponse(userId, message);
|
|
158
|
+
if (cached) return new Response(cached);
|
|
159
|
+
|
|
160
|
+
const result = await streamText({ model, messages: [{ role: 'user', content: message }] });
|
|
161
|
+
const text = await result.text;
|
|
162
|
+
|
|
163
|
+
await controls.cacheResponse(userId, message, text);
|
|
164
|
+
await controls.trackTokenUsage(userId, result.usage.promptTokens, result.usage.completionTokens);
|
|
165
|
+
|
|
166
|
+
return new Response(text);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Express Middleware
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import express from 'express';
|
|
174
|
+
import { CostControls } from 'ai-cost-controls';
|
|
175
|
+
|
|
176
|
+
const controls = new CostControls();
|
|
177
|
+
const app = express();
|
|
178
|
+
|
|
179
|
+
app.use('/ai', async (req, res, next) => {
|
|
180
|
+
const userId = req.user.id;
|
|
181
|
+
|
|
182
|
+
if (!(await controls.checkRateLimit(userId))) {
|
|
183
|
+
return res.status(429).json({ error: 'Rate limited' });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
next();
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## API Reference
|
|
191
|
+
|
|
192
|
+
### `new CostControls(options?: CostControlsOptions)`
|
|
193
|
+
|
|
194
|
+
Creates a new instance.
|
|
195
|
+
|
|
196
|
+
### `checkRateLimit(userId: string): Promise<boolean>`
|
|
197
|
+
|
|
198
|
+
Returns `true` if the request is allowed, `false` if rate-limited.
|
|
199
|
+
|
|
200
|
+
### `getCachedResponse(userId: string, message: string): Promise<string | null>`
|
|
201
|
+
|
|
202
|
+
Returns a cached response or `null`. Cache keys are case-insensitive and trim-aware.
|
|
203
|
+
|
|
204
|
+
### `cacheResponse(userId: string, message: string, response: string): Promise<void>`
|
|
205
|
+
|
|
206
|
+
Stores a response in the cache.
|
|
207
|
+
|
|
208
|
+
### `trackTokenUsage(userId: string, inputTokens: number, outputTokens: number): Promise<boolean>`
|
|
209
|
+
|
|
210
|
+
Tracks token usage. Returns `false` if the daily or monthly budget would be exceeded.
|
|
211
|
+
|
|
212
|
+
### `getTokenUsage(userId: string, period: 'daily' | 'monthly'): Promise<number>`
|
|
213
|
+
|
|
214
|
+
Returns current token usage for the specified period.
|
|
215
|
+
|
|
216
|
+
### `getConfig(): Promise<CostControlsConfig>`
|
|
217
|
+
|
|
218
|
+
Returns the resolved configuration (static defaults merged with `configLoader` result).
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CostControls = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const in_memory_backend_1 = require("./in-memory-backend");
|
|
6
|
+
const interfaces_1 = require("./interfaces");
|
|
7
|
+
const NOOP_LOGGER = {
|
|
8
|
+
debug() { },
|
|
9
|
+
warn() { }
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Framework-agnostic AI cost controls: rate limiting, response caching,
|
|
13
|
+
* and token budget tracking with pluggable cache backends.
|
|
14
|
+
*/
|
|
15
|
+
class CostControls {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
/** In-memory rate limit store (always local — not backed by CacheBackend). */
|
|
18
|
+
this.rateLimits = new Map();
|
|
19
|
+
this.backend = options?.cacheBackend ?? new in_memory_backend_1.InMemoryCacheBackend();
|
|
20
|
+
this.configLoader = options?.configLoader;
|
|
21
|
+
this.logger = options?.logger ?? NOOP_LOGGER;
|
|
22
|
+
this.staticConfig = { ...interfaces_1.DEFAULT_CONFIG, ...options?.config };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve config: static defaults merged with configLoader result (if provided).
|
|
26
|
+
*/
|
|
27
|
+
async getConfig() {
|
|
28
|
+
if (!this.configLoader) {
|
|
29
|
+
return this.staticConfig;
|
|
30
|
+
}
|
|
31
|
+
const dynamic = await this.configLoader();
|
|
32
|
+
return {
|
|
33
|
+
...this.staticConfig,
|
|
34
|
+
...dynamic
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check and enforce per-user rate limiting.
|
|
39
|
+
* Returns true if the request is allowed, false if rate-limited.
|
|
40
|
+
*/
|
|
41
|
+
async checkRateLimit(userId) {
|
|
42
|
+
const config = await this.getConfig();
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const windowMs = 60000;
|
|
45
|
+
let entry = this.rateLimits.get(userId);
|
|
46
|
+
if (!entry) {
|
|
47
|
+
entry = { timestamps: [] };
|
|
48
|
+
this.rateLimits.set(userId, entry);
|
|
49
|
+
}
|
|
50
|
+
entry.timestamps = entry.timestamps.filter((ts) => now - ts < windowMs);
|
|
51
|
+
if (entry.timestamps.length >= config.rateLimitPerMinute) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
entry.timestamps.push(now);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get a cached response for a query, if one exists within the cache TTL.
|
|
59
|
+
*/
|
|
60
|
+
async getCachedResponse(userId, message) {
|
|
61
|
+
const cacheKey = this.buildCacheKey(userId, message);
|
|
62
|
+
try {
|
|
63
|
+
const cached = await this.backend.get(cacheKey);
|
|
64
|
+
if (cached) {
|
|
65
|
+
this.logger.debug(`Cache hit for user ${userId}`);
|
|
66
|
+
return cached;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.logger.warn(`Cache read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Store a response in the cache for future identical queries.
|
|
76
|
+
*/
|
|
77
|
+
async cacheResponse(userId, message, response) {
|
|
78
|
+
const config = await this.getConfig();
|
|
79
|
+
const cacheKey = this.buildCacheKey(userId, message);
|
|
80
|
+
const ttlMs = config.cacheTtlSeconds * 1000;
|
|
81
|
+
try {
|
|
82
|
+
await this.backend.set(cacheKey, response, ttlMs);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
this.logger.warn(`Cache write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Track token usage for a user. Returns false if budget is exceeded.
|
|
90
|
+
*/
|
|
91
|
+
async trackTokenUsage(userId, inputTokens, outputTokens) {
|
|
92
|
+
const config = await this.getConfig();
|
|
93
|
+
const dailyKey = this.buildTokenKey(userId, 'daily');
|
|
94
|
+
const monthlyKey = this.buildTokenKey(userId, 'monthly');
|
|
95
|
+
const totalTokens = inputTokens + outputTokens;
|
|
96
|
+
// Read current usage
|
|
97
|
+
let dailyUsage = 0;
|
|
98
|
+
let monthlyUsage = 0;
|
|
99
|
+
try {
|
|
100
|
+
const [dailyRaw, monthlyRaw] = await Promise.all([
|
|
101
|
+
this.backend.get(dailyKey),
|
|
102
|
+
this.backend.get(monthlyKey)
|
|
103
|
+
]);
|
|
104
|
+
dailyUsage = dailyRaw ? parseInt(dailyRaw, 10) : 0;
|
|
105
|
+
monthlyUsage = monthlyRaw ? parseInt(monthlyRaw, 10) : 0;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
this.logger.warn(`Token usage read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
109
|
+
}
|
|
110
|
+
if (dailyUsage + totalTokens > config.dailyTokenBudget) {
|
|
111
|
+
this.logger.warn(`Daily token budget exceeded for user ${userId}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (monthlyUsage + totalTokens > config.monthlyTokenBudget) {
|
|
115
|
+
this.logger.warn(`Monthly token budget exceeded for user ${userId}`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
// Write updated usage with appropriate TTLs
|
|
119
|
+
const endOfDay = new Date();
|
|
120
|
+
endOfDay.setHours(23, 59, 59, 999);
|
|
121
|
+
const dailyTtl = endOfDay.getTime() - Date.now();
|
|
122
|
+
const endOfMonth = new Date();
|
|
123
|
+
endOfMonth.setMonth(endOfMonth.getMonth() + 1, 1);
|
|
124
|
+
endOfMonth.setHours(0, 0, 0, 0);
|
|
125
|
+
const monthlyTtl = endOfMonth.getTime() - Date.now();
|
|
126
|
+
try {
|
|
127
|
+
await Promise.all([
|
|
128
|
+
this.backend.set(dailyKey, String(dailyUsage + totalTokens), dailyTtl),
|
|
129
|
+
this.backend.set(monthlyKey, String(monthlyUsage + totalTokens), monthlyTtl)
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
this.logger.warn(`Token usage write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get current token usage for a user within the specified period.
|
|
139
|
+
*/
|
|
140
|
+
async getTokenUsage(userId, period) {
|
|
141
|
+
const key = this.buildTokenKey(userId, period);
|
|
142
|
+
try {
|
|
143
|
+
const value = await this.backend.get(key);
|
|
144
|
+
return value ? parseInt(value, 10) : 0;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// --- Private helpers ---
|
|
151
|
+
buildCacheKey(userId, message) {
|
|
152
|
+
const hash = (0, node_crypto_1.createHash)('sha256')
|
|
153
|
+
.update(message.toLowerCase().trim())
|
|
154
|
+
.digest('hex')
|
|
155
|
+
.substring(0, 16);
|
|
156
|
+
return `ai-cache:${userId}:${hash}`;
|
|
157
|
+
}
|
|
158
|
+
buildTokenKey(userId, period) {
|
|
159
|
+
const now = new Date();
|
|
160
|
+
if (period === 'daily') {
|
|
161
|
+
return `ai-tokens:${userId}:daily:${now.toISOString().slice(0, 10)}`;
|
|
162
|
+
}
|
|
163
|
+
return `ai-tokens:${userId}:monthly:${now.toISOString().slice(0, 7)}`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.CostControls = CostControls;
|
|
167
|
+
//# sourceMappingURL=cost-controls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-controls.js","sourceRoot":"","sources":["../../src/cost-controls.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryCacheBackend = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Zero-dependency in-memory cache backend.
|
|
6
|
+
* Suitable for single-process deployments. For multi-process or serverless,
|
|
7
|
+
* provide a Redis/KV CacheBackend instead.
|
|
8
|
+
*/
|
|
9
|
+
class InMemoryCacheBackend {
|
|
10
|
+
constructor(maxSize = 10000) {
|
|
11
|
+
this.store = new Map();
|
|
12
|
+
this.maxSize = maxSize;
|
|
13
|
+
}
|
|
14
|
+
async get(key) {
|
|
15
|
+
const entry = this.store.get(key);
|
|
16
|
+
if (!entry) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (entry.expiresAt <= Date.now()) {
|
|
20
|
+
this.store.delete(key);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return entry.value;
|
|
24
|
+
}
|
|
25
|
+
async set(key, value, ttlMs) {
|
|
26
|
+
// Evict oldest entries if at capacity
|
|
27
|
+
if (this.store.size >= this.maxSize && !this.store.has(key)) {
|
|
28
|
+
const firstKey = this.store.keys().next().value;
|
|
29
|
+
if (firstKey !== undefined) {
|
|
30
|
+
this.store.delete(firstKey);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
this.store.set(key, {
|
|
34
|
+
value,
|
|
35
|
+
expiresAt: Date.now() + ttlMs
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Number of entries currently stored (including expired). */
|
|
39
|
+
get size() {
|
|
40
|
+
return this.store.size;
|
|
41
|
+
}
|
|
42
|
+
/** Remove all entries. */
|
|
43
|
+
clear() {
|
|
44
|
+
this.store.clear();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.InMemoryCacheBackend = InMemoryCacheBackend;
|
|
48
|
+
//# sourceMappingURL=in-memory-backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-backend.js","sourceRoot":"","sources":["../../src/in-memory-backend.ts"],"names":[],"mappings":";;;AAEA;;;;GAIG;AACH,MAAa,oBAAoB;IAI/B,YAAY,OAAO,GAAG,KAAM;QAHX,UAAK,GAAG,IAAI,GAAG,EAAgD,CAAC;QAI/E,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAElC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAClC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,KAAa;QACjD,sCAAsC;QACtC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAChD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;YAClB,KAAK;YACL,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;SAC9B,CAAC,CAAC;IACL,CAAC;IAED,8DAA8D;IAC9D,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IAED,0BAA0B;IAC1B,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACF;AA/CD,oDA+CC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_CONFIG = exports.InMemoryCacheBackend = exports.CostControls = void 0;
|
|
4
|
+
var cost_controls_1 = require("./cost-controls");
|
|
5
|
+
Object.defineProperty(exports, "CostControls", { enumerable: true, get: function () { return cost_controls_1.CostControls; } });
|
|
6
|
+
var in_memory_backend_1 = require("./in-memory-backend");
|
|
7
|
+
Object.defineProperty(exports, "InMemoryCacheBackend", { enumerable: true, get: function () { return in_memory_backend_1.InMemoryCacheBackend; } });
|
|
8
|
+
var interfaces_1 = require("./interfaces");
|
|
9
|
+
Object.defineProperty(exports, "DEFAULT_CONFIG", { enumerable: true, get: function () { return interfaces_1.DEFAULT_CONFIG; } });
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,iDAA+C;AAAtC,6GAAA,YAAY,OAAA;AACrB,yDAA2D;AAAlD,yHAAA,oBAAoB,OAAA;AAC7B,2CAMsB;AAFpB,4GAAA,cAAc,OAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_CONFIG = void 0;
|
|
4
|
+
/** Default configuration values. */
|
|
5
|
+
exports.DEFAULT_CONFIG = {
|
|
6
|
+
rateLimitPerMinute: 20,
|
|
7
|
+
cacheTtlSeconds: 300,
|
|
8
|
+
dailyTokenBudget: 100000,
|
|
9
|
+
monthlyTokenBudget: 2000000
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=interfaces.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interfaces.js","sourceRoot":"","sources":["../../src/interfaces.ts"],"names":[],"mappings":";;;AAmCA,oCAAoC;AACvB,QAAA,cAAc,GAAuB;IAChD,kBAAkB,EAAE,EAAE;IACtB,eAAe,EAAE,GAAG;IACpB,gBAAgB,EAAE,MAAO;IACzB,kBAAkB,EAAE,OAAS;CAC9B,CAAC"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { InMemoryCacheBackend } from './in-memory-backend';
|
|
3
|
+
import { DEFAULT_CONFIG } from './interfaces';
|
|
4
|
+
const NOOP_LOGGER = {
|
|
5
|
+
debug() { },
|
|
6
|
+
warn() { }
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Framework-agnostic AI cost controls: rate limiting, response caching,
|
|
10
|
+
* and token budget tracking with pluggable cache backends.
|
|
11
|
+
*/
|
|
12
|
+
export class CostControls {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
/** In-memory rate limit store (always local — not backed by CacheBackend). */
|
|
15
|
+
this.rateLimits = new Map();
|
|
16
|
+
this.backend = options?.cacheBackend ?? new InMemoryCacheBackend();
|
|
17
|
+
this.configLoader = options?.configLoader;
|
|
18
|
+
this.logger = options?.logger ?? NOOP_LOGGER;
|
|
19
|
+
this.staticConfig = { ...DEFAULT_CONFIG, ...options?.config };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve config: static defaults merged with configLoader result (if provided).
|
|
23
|
+
*/
|
|
24
|
+
async getConfig() {
|
|
25
|
+
if (!this.configLoader) {
|
|
26
|
+
return this.staticConfig;
|
|
27
|
+
}
|
|
28
|
+
const dynamic = await this.configLoader();
|
|
29
|
+
return {
|
|
30
|
+
...this.staticConfig,
|
|
31
|
+
...dynamic
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check and enforce per-user rate limiting.
|
|
36
|
+
* Returns true if the request is allowed, false if rate-limited.
|
|
37
|
+
*/
|
|
38
|
+
async checkRateLimit(userId) {
|
|
39
|
+
const config = await this.getConfig();
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const windowMs = 60000;
|
|
42
|
+
let entry = this.rateLimits.get(userId);
|
|
43
|
+
if (!entry) {
|
|
44
|
+
entry = { timestamps: [] };
|
|
45
|
+
this.rateLimits.set(userId, entry);
|
|
46
|
+
}
|
|
47
|
+
entry.timestamps = entry.timestamps.filter((ts) => now - ts < windowMs);
|
|
48
|
+
if (entry.timestamps.length >= config.rateLimitPerMinute) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
entry.timestamps.push(now);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get a cached response for a query, if one exists within the cache TTL.
|
|
56
|
+
*/
|
|
57
|
+
async getCachedResponse(userId, message) {
|
|
58
|
+
const cacheKey = this.buildCacheKey(userId, message);
|
|
59
|
+
try {
|
|
60
|
+
const cached = await this.backend.get(cacheKey);
|
|
61
|
+
if (cached) {
|
|
62
|
+
this.logger.debug(`Cache hit for user ${userId}`);
|
|
63
|
+
return cached;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
this.logger.warn(`Cache read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Store a response in the cache for future identical queries.
|
|
73
|
+
*/
|
|
74
|
+
async cacheResponse(userId, message, response) {
|
|
75
|
+
const config = await this.getConfig();
|
|
76
|
+
const cacheKey = this.buildCacheKey(userId, message);
|
|
77
|
+
const ttlMs = config.cacheTtlSeconds * 1000;
|
|
78
|
+
try {
|
|
79
|
+
await this.backend.set(cacheKey, response, ttlMs);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
this.logger.warn(`Cache write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Track token usage for a user. Returns false if budget is exceeded.
|
|
87
|
+
*/
|
|
88
|
+
async trackTokenUsage(userId, inputTokens, outputTokens) {
|
|
89
|
+
const config = await this.getConfig();
|
|
90
|
+
const dailyKey = this.buildTokenKey(userId, 'daily');
|
|
91
|
+
const monthlyKey = this.buildTokenKey(userId, 'monthly');
|
|
92
|
+
const totalTokens = inputTokens + outputTokens;
|
|
93
|
+
// Read current usage
|
|
94
|
+
let dailyUsage = 0;
|
|
95
|
+
let monthlyUsage = 0;
|
|
96
|
+
try {
|
|
97
|
+
const [dailyRaw, monthlyRaw] = await Promise.all([
|
|
98
|
+
this.backend.get(dailyKey),
|
|
99
|
+
this.backend.get(monthlyKey)
|
|
100
|
+
]);
|
|
101
|
+
dailyUsage = dailyRaw ? parseInt(dailyRaw, 10) : 0;
|
|
102
|
+
monthlyUsage = monthlyRaw ? parseInt(monthlyRaw, 10) : 0;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
this.logger.warn(`Token usage read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
if (dailyUsage + totalTokens > config.dailyTokenBudget) {
|
|
108
|
+
this.logger.warn(`Daily token budget exceeded for user ${userId}`);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (monthlyUsage + totalTokens > config.monthlyTokenBudget) {
|
|
112
|
+
this.logger.warn(`Monthly token budget exceeded for user ${userId}`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
// Write updated usage with appropriate TTLs
|
|
116
|
+
const endOfDay = new Date();
|
|
117
|
+
endOfDay.setHours(23, 59, 59, 999);
|
|
118
|
+
const dailyTtl = endOfDay.getTime() - Date.now();
|
|
119
|
+
const endOfMonth = new Date();
|
|
120
|
+
endOfMonth.setMonth(endOfMonth.getMonth() + 1, 1);
|
|
121
|
+
endOfMonth.setHours(0, 0, 0, 0);
|
|
122
|
+
const monthlyTtl = endOfMonth.getTime() - Date.now();
|
|
123
|
+
try {
|
|
124
|
+
await Promise.all([
|
|
125
|
+
this.backend.set(dailyKey, String(dailyUsage + totalTokens), dailyTtl),
|
|
126
|
+
this.backend.set(monthlyKey, String(monthlyUsage + totalTokens), monthlyTtl)
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
this.logger.warn(`Token usage write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get current token usage for a user within the specified period.
|
|
136
|
+
*/
|
|
137
|
+
async getTokenUsage(userId, period) {
|
|
138
|
+
const key = this.buildTokenKey(userId, period);
|
|
139
|
+
try {
|
|
140
|
+
const value = await this.backend.get(key);
|
|
141
|
+
return value ? parseInt(value, 10) : 0;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// --- Private helpers ---
|
|
148
|
+
buildCacheKey(userId, message) {
|
|
149
|
+
const hash = createHash('sha256')
|
|
150
|
+
.update(message.toLowerCase().trim())
|
|
151
|
+
.digest('hex')
|
|
152
|
+
.substring(0, 16);
|
|
153
|
+
return `ai-cache:${userId}:${hash}`;
|
|
154
|
+
}
|
|
155
|
+
buildTokenKey(userId, period) {
|
|
156
|
+
const now = new Date();
|
|
157
|
+
if (period === 'daily') {
|
|
158
|
+
return `ai-tokens:${userId}:daily:${now.toISOString().slice(0, 10)}`;
|
|
159
|
+
}
|
|
160
|
+
return `ai-tokens:${userId}:monthly:${now.toISOString().slice(0, 7)}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=cost-controls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-controls.js","sourceRoot":"","sources":["../../src/cost-controls.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency in-memory cache backend.
|
|
3
|
+
* Suitable for single-process deployments. For multi-process or serverless,
|
|
4
|
+
* provide a Redis/KV CacheBackend instead.
|
|
5
|
+
*/
|
|
6
|
+
export class InMemoryCacheBackend {
|
|
7
|
+
constructor(maxSize = 10000) {
|
|
8
|
+
this.store = new Map();
|
|
9
|
+
this.maxSize = maxSize;
|
|
10
|
+
}
|
|
11
|
+
async get(key) {
|
|
12
|
+
const entry = this.store.get(key);
|
|
13
|
+
if (!entry) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (entry.expiresAt <= Date.now()) {
|
|
17
|
+
this.store.delete(key);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return entry.value;
|
|
21
|
+
}
|
|
22
|
+
async set(key, value, ttlMs) {
|
|
23
|
+
// Evict oldest entries if at capacity
|
|
24
|
+
if (this.store.size >= this.maxSize && !this.store.has(key)) {
|
|
25
|
+
const firstKey = this.store.keys().next().value;
|
|
26
|
+
if (firstKey !== undefined) {
|
|
27
|
+
this.store.delete(firstKey);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
this.store.set(key, {
|
|
31
|
+
value,
|
|
32
|
+
expiresAt: Date.now() + ttlMs
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Number of entries currently stored (including expired). */
|
|
36
|
+
get size() {
|
|
37
|
+
return this.store.size;
|
|
38
|
+
}
|
|
39
|
+
/** Remove all entries. */
|
|
40
|
+
clear() {
|
|
41
|
+
this.store.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=in-memory-backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-backend.js","sourceRoot":"","sources":["../../src/in-memory-backend.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,OAAO,oBAAoB;IAI/B,YAAY,OAAO,GAAG,KAAM;QAHX,UAAK,GAAG,IAAI,GAAG,EAAgD,CAAC;QAI/E,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAElC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAClC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,KAAa;QACjD,sCAAsC;QACtC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAChD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;YAClB,KAAK;YACL,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;SAC9B,CAAC,CAAC;IACL,CAAC;IAED,8DAA8D;IAC9D,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IAED,0BAA0B;IAC1B,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAIL,cAAc,EAEf,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interfaces.js","sourceRoot":"","sources":["../../src/interfaces.ts"],"names":[],"mappings":"AAmCA,oCAAoC;AACpC,MAAM,CAAC,MAAM,cAAc,GAAuB;IAChD,kBAAkB,EAAE,EAAE;IACtB,eAAe,EAAE,GAAG;IACpB,gBAAgB,EAAE,MAAO;IACzB,kBAAkB,EAAE,OAAS;CAC9B,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CostControlsConfig, CostControlsOptions } from './interfaces';
|
|
2
|
+
/**
|
|
3
|
+
* Framework-agnostic AI cost controls: rate limiting, response caching,
|
|
4
|
+
* and token budget tracking with pluggable cache backends.
|
|
5
|
+
*/
|
|
6
|
+
export declare class CostControls {
|
|
7
|
+
private readonly backend;
|
|
8
|
+
private readonly staticConfig;
|
|
9
|
+
private readonly configLoader?;
|
|
10
|
+
private readonly logger;
|
|
11
|
+
/** In-memory rate limit store (always local — not backed by CacheBackend). */
|
|
12
|
+
private readonly rateLimits;
|
|
13
|
+
constructor(options?: CostControlsOptions);
|
|
14
|
+
/**
|
|
15
|
+
* Resolve config: static defaults merged with configLoader result (if provided).
|
|
16
|
+
*/
|
|
17
|
+
getConfig(): Promise<CostControlsConfig>;
|
|
18
|
+
/**
|
|
19
|
+
* Check and enforce per-user rate limiting.
|
|
20
|
+
* Returns true if the request is allowed, false if rate-limited.
|
|
21
|
+
*/
|
|
22
|
+
checkRateLimit(userId: string): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Get a cached response for a query, if one exists within the cache TTL.
|
|
25
|
+
*/
|
|
26
|
+
getCachedResponse(userId: string, message: string): Promise<string | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Store a response in the cache for future identical queries.
|
|
29
|
+
*/
|
|
30
|
+
cacheResponse(userId: string, message: string, response: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Track token usage for a user. Returns false if budget is exceeded.
|
|
33
|
+
*/
|
|
34
|
+
trackTokenUsage(userId: string, inputTokens: number, outputTokens: number): Promise<boolean>;
|
|
35
|
+
/**
|
|
36
|
+
* Get current token usage for a user within the specified period.
|
|
37
|
+
*/
|
|
38
|
+
getTokenUsage(userId: string, period: 'daily' | 'monthly'): Promise<number>;
|
|
39
|
+
private buildCacheKey;
|
|
40
|
+
private buildTokenKey;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=cost-controls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-controls.d.ts","sourceRoot":"","sources":["../../src/cost-controls.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,kBAAkB,EAClB,mBAAmB,EAGpB,MAAM,cAAc,CAAC;AAWtB;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAA6C;IAC3E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,8EAA8E;IAC9E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqC;gBAEpD,OAAO,CAAC,EAAE,mBAAmB;IAOzC;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAa9C;;;OAGG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAsBtD;;OAEG;IACG,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAmBzB;;OAEG;IACG,aAAa,CACjB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC;IAchB;;OAEG;IACG,eAAe,CACnB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,OAAO,CAAC;IA6DnB;;OAEG;IACG,aAAa,CACjB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,GAAG,SAAS,GAC1B,OAAO,CAAC,MAAM,CAAC;IAalB,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,aAAa;CAYtB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CacheBackend } from './interfaces';
|
|
2
|
+
/**
|
|
3
|
+
* Zero-dependency in-memory cache backend.
|
|
4
|
+
* Suitable for single-process deployments. For multi-process or serverless,
|
|
5
|
+
* provide a Redis/KV CacheBackend instead.
|
|
6
|
+
*/
|
|
7
|
+
export declare class InMemoryCacheBackend implements CacheBackend {
|
|
8
|
+
private readonly store;
|
|
9
|
+
private readonly maxSize;
|
|
10
|
+
constructor(maxSize?: number);
|
|
11
|
+
get(key: string): Promise<string | null>;
|
|
12
|
+
set(key: string, value: string, ttlMs: number): Promise<void>;
|
|
13
|
+
/** Number of entries currently stored (including expired). */
|
|
14
|
+
get size(): number;
|
|
15
|
+
/** Remove all entries. */
|
|
16
|
+
clear(): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=in-memory-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-backend.d.ts","sourceRoot":"","sources":["../../src/in-memory-backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;GAIG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2D;IACjF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,OAAO,SAAS;IAItB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAexC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAenE,8DAA8D;IAC9D,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,0BAA0B;IAC1B,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,MAAM,EACP,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable cache backend interface.
|
|
3
|
+
* Implement with ioredis, node-redis, @upstash/redis, Cloudflare KV, etc.
|
|
4
|
+
*/
|
|
5
|
+
export interface CacheBackend {
|
|
6
|
+
get(key: string): Promise<string | null>;
|
|
7
|
+
set(key: string, value: string, ttlMs: number): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/** Cost controls configuration values. */
|
|
10
|
+
export interface CostControlsConfig {
|
|
11
|
+
rateLimitPerMinute: number;
|
|
12
|
+
cacheTtlSeconds: number;
|
|
13
|
+
dailyTokenBudget: number;
|
|
14
|
+
monthlyTokenBudget: number;
|
|
15
|
+
}
|
|
16
|
+
/** Minimal logger interface — bring your own logger. */
|
|
17
|
+
export interface Logger {
|
|
18
|
+
debug(msg: string): void;
|
|
19
|
+
warn(msg: string): void;
|
|
20
|
+
}
|
|
21
|
+
/** Constructor options for CostControls. */
|
|
22
|
+
export interface CostControlsOptions {
|
|
23
|
+
/** Static config overrides (merged with defaults). */
|
|
24
|
+
config?: Partial<CostControlsConfig>;
|
|
25
|
+
/** Cache backend — defaults to InMemoryCacheBackend. */
|
|
26
|
+
cacheBackend?: CacheBackend;
|
|
27
|
+
/** Dynamic config loader (e.g., from DB). Called on each operation. */
|
|
28
|
+
configLoader?: () => Promise<Partial<CostControlsConfig>>;
|
|
29
|
+
/** Optional logger. */
|
|
30
|
+
logger?: Logger;
|
|
31
|
+
}
|
|
32
|
+
/** Default configuration values. */
|
|
33
|
+
export declare const DEFAULT_CONFIG: CostControlsConfig;
|
|
34
|
+
//# sourceMappingURL=interfaces.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../../src/interfaces.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D;AAED,0CAA0C;AAC1C,MAAM,WAAW,kBAAkB;IACjC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,4CAA4C;AAC5C,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,MAAM,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACrC,wDAAwD;IACxD,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC1D,uBAAuB;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,oCAAoC;AACpC,eAAO,MAAM,cAAc,EAAE,kBAK5B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-cost-controls",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic AI cost controls: per-user rate limiting, token budget tracking, and response caching with pluggable backends.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"cost-controls",
|
|
8
|
+
"rate-limiting",
|
|
9
|
+
"token-budget",
|
|
10
|
+
"caching",
|
|
11
|
+
"llm",
|
|
12
|
+
"openai",
|
|
13
|
+
"anthropic",
|
|
14
|
+
"vercel-ai-sdk"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Ray Ockenfels",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/roaming-rockenfels/ai-cost-controls.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/roaming-rockenfels/ai-cost-controls/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/roaming-rockenfels/ai-cost-controls#readme",
|
|
26
|
+
"main": "./dist/cjs/index.js",
|
|
27
|
+
"module": "./dist/esm/index.js",
|
|
28
|
+
"types": "./dist/types/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/types/index.d.ts",
|
|
33
|
+
"default": "./dist/esm/index.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/types/index.d.ts",
|
|
37
|
+
"default": "./dist/cjs/index.js"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "npm run build:cjs && npm run build:esm && npm run build:types",
|
|
48
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
49
|
+
"build:esm": "tsc -p tsconfig.esm.json",
|
|
50
|
+
"build:types": "tsc -p tsconfig.types.json",
|
|
51
|
+
"test": "jest",
|
|
52
|
+
"lint": "eslint src/ tests/ --ext .ts",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"prepare": "npm run build",
|
|
55
|
+
"prepublishOnly": "npm test"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/jest": "^29.5.0",
|
|
59
|
+
"@types/node": "^22.0.0",
|
|
60
|
+
"jest": "^29.7.0",
|
|
61
|
+
"ts-jest": "^29.1.0",
|
|
62
|
+
"ts-node": "^10.9.2",
|
|
63
|
+
"typescript": "^5.4.0"
|
|
64
|
+
}
|
|
65
|
+
}
|