adaptive-memory-multi-model-router 2.13.18 → 2.13.20
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/.dockerignore +82 -0
- package/.env.example +303 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +83 -12
- package/.github/ISSUE_TEMPLATE/config.yml +12 -6
- package/.github/ISSUE_TEMPLATE/feature_request.md +61 -10
- package/.github/PULL_REQUEST_TEMPLATE.md +53 -26
- package/.github/dependabot.yml +9 -0
- package/.github/workflows/codeql.yml +38 -0
- package/.github/workflows/npm-publish.yml +20 -0
- package/.github/workflows/stale.yml +56 -0
- package/ARCHITECTURE.md +346 -0
- package/AUDIT_REPORT.md +28 -0
- package/CHANGELOG.md +386 -22
- package/CONTRIBUTORS.md +20 -0
- package/Dockerfile +53 -0
- package/Dockerfile.proxy +33 -0
- package/PR_STATUS_REPORT.md +148 -0
- package/README.md +22 -0
- package/RUNKIT.md +83 -0
- package/_schema.html +61 -15
- package/articles/AI_AGENT_LLM_ROUTING.md +150 -0
- package/articles/FROM_ZERO_TO_10K.md +107 -0
- package/articles/LLM_BENCHMARK_DEEP_DIVE.md +153 -0
- package/articles/TWEETS_10K_DOWNLOADS.md +47 -0
- package/articles/TWEETS_BENCHMARK_FIRST.md +46 -0
- package/articles/TWEETS_MCP_PLAY.md +51 -0
- package/articles/TWEETS_SEQUENTIAL_BROKEN.md +49 -0
- package/articles/TWEETS_WHY_BUILD.md +54 -0
- package/benchmark-results.json +26 -45
- package/cli/a3m +840 -0
- package/demo/package.json +13 -0
- package/demo/public/index.html +762 -0
- package/demo/server.js +405 -0
- package/dist/cli.js +4 -0
- package/docker-compose.yml +74 -0
- package/docs/.nojekyll +0 -0
- package/docs/BENCHMARK.md +96 -22
- package/docs/_config.yml +49 -0
- package/docs/api.html +513 -0
- package/docs/benchmark.html +387 -0
- package/docs/cli-cheatsheet.md +339 -0
- package/docs/comparison.md +108 -0
- package/docs/curl-examples.md +247 -0
- package/docs/index.html +390 -99
- package/docs/openapi.yaml +1318 -0
- package/docs/quick-start.html +366 -0
- package/docs/robots.txt +1 -1
- package/docs/sitemap.xml +23 -5
- package/docs/styles.css +682 -0
- package/examples/README.md +61 -0
- package/examples/a3m-sdk.js +124 -0
- package/examples/basic-route.js +54 -0
- package/examples/chat-loop.js +202 -0
- package/examples/classify-then-route.js +102 -0
- package/examples/cost-compare.js +120 -0
- package/examples/ensemble.js +160 -0
- package/integrations/langchain/README.md +216 -0
- package/integrations/langchain/a3m_langchain.ts +1360 -0
- package/integrations/langchain/example.ts +287 -0
- package/integrations/vercel-ai-sdk/README.md +49 -0
- package/integrations/vercel-ai-sdk/a3m_provider.ts +78 -0
- package/integrations/vercel-ai-sdk/example.ts +25 -0
- package/llms-full.txt +43 -0
- package/llms.txt +9 -0
- package/mcp-server/README.md +188 -0
- package/mcp-server/package.json +29 -0
- package/mcp-server/src/index.ts +744 -0
- package/mcp-server/tsconfig.json +19 -0
- package/package.json +3 -3
- package/proxy/README.md +227 -0
- package/proxy/package-lock.json +831 -0
- package/proxy/package.json +17 -0
- package/proxy/rate-limit.js +145 -0
- package/proxy/rate-limit.test.js +311 -0
- package/proxy/server.js +970 -0
- package/scripts/banner.js +29 -0
- package/scripts/compare-providers.sh +230 -0
- package/scripts/cross_post.py +443 -0
- package/scripts/publish_fcc.py +106 -0
- package/scripts/push-to-gitee.sh +52 -0
- package/src/tui/dashboard.ts +13 -0
- package/tests/__mocks__/tokenUtils.ts +22 -0
- package/tests/memory/episodicMemory.test.ts +227 -0
- package/tests/package-lock.json +1628 -0
- package/tests/package.json +18 -0
- package/tests/routing/ensembleVoting.test.ts +236 -0
- package/tests/routing/providerRetry.test.ts +360 -0
- package/tests/routing/queryTypePresets.test.ts +206 -0
- package/tests/tsconfig.json +21 -0
- package/tests/vitest.config.ts +18 -0
- package/.env +0 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "a3m-proxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenAI-compatible proxy for A3M Router — intelligent multi-LLM routing",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node server.js",
|
|
8
|
+
"dev": "node --watch server.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"express": "^4.18.0"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT"
|
|
17
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A3M Router — In-Memory Rate Limiter Middleware
|
|
5
|
+
*
|
|
6
|
+
* Sliding-window rate limiter for Express. Tracks request timestamps per IP
|
|
7
|
+
* and rejects clients that exceed the configured threshold within the window.
|
|
8
|
+
*
|
|
9
|
+
* Configuration (environment variables):
|
|
10
|
+
* RATE_LIMIT_WINDOW_MS — time window in milliseconds (default: 60000)
|
|
11
|
+
* RATE_LIMIT_MAX — max requests per window (default: 60)
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const RateLimiter = require("./rate-limit");
|
|
15
|
+
* app.use(new RateLimiter().middleware());
|
|
16
|
+
*
|
|
17
|
+
* Or with custom options:
|
|
18
|
+
* app.use(new RateLimiter({ windowMs: 30_000, maxRequests: 30 }).middleware());
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
class RateLimiter {
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} options
|
|
24
|
+
* @param {number} [options.windowMs] — sliding window duration in ms
|
|
25
|
+
* @param {number} [options.maxRequests] — max requests allowed per window
|
|
26
|
+
*/
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.windowMs = options.windowMs || parseInt(process.env.RATE_LIMIT_WINDOW_MS || "60000", 10);
|
|
29
|
+
this.maxRequests = options.maxRequests || parseInt(process.env.RATE_LIMIT_MAX || "60", 10);
|
|
30
|
+
this.clients = new Map();
|
|
31
|
+
|
|
32
|
+
// Periodic cleanup every 60s to evict stale entries
|
|
33
|
+
this._cleanupInterval = setInterval(() => this._cleanup(), 60_000);
|
|
34
|
+
this._cleanupInterval.unref();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the Express middleware function.
|
|
39
|
+
*/
|
|
40
|
+
middleware() {
|
|
41
|
+
return (req, res, next) => {
|
|
42
|
+
// Trust X-Forwarded-For if behind a proxy
|
|
43
|
+
const key = req.headers["x-forwarded-for"]?.split(",")[0]?.trim()
|
|
44
|
+
|| req.ip
|
|
45
|
+
|| req.socket?.remoteAddress
|
|
46
|
+
|| "unknown";
|
|
47
|
+
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
|
|
50
|
+
if (!this.clients.has(key)) {
|
|
51
|
+
this.clients.set(key, []);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const timestamps = this.clients.get(key);
|
|
55
|
+
// Keep only timestamps within the current window
|
|
56
|
+
const windowStart = now - this.windowMs;
|
|
57
|
+
const recent = timestamps.filter((t) => t >= windowStart);
|
|
58
|
+
|
|
59
|
+
if (recent.length >= this.maxRequests) {
|
|
60
|
+
const retryAfter = Math.ceil(
|
|
61
|
+
(recent[0] + this.windowMs - now) / 1000
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
res.setHeader("Retry-After", String(retryAfter));
|
|
65
|
+
res.setHeader("X-RateLimit-Limit", String(this.maxRequests));
|
|
66
|
+
res.setHeader("X-RateLimit-Remaining", "0");
|
|
67
|
+
res.setHeader("X-RateLimit-Reset", String(Math.ceil((recent[0] + this.windowMs) / 1000)));
|
|
68
|
+
|
|
69
|
+
return res.status(429).json({
|
|
70
|
+
error: {
|
|
71
|
+
message: "Too many requests. Please slow down.",
|
|
72
|
+
type: "rate_limit_error",
|
|
73
|
+
code: 429,
|
|
74
|
+
retry_after: retryAfter,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
recent.push(now);
|
|
80
|
+
this.clients.set(key, recent);
|
|
81
|
+
|
|
82
|
+
// Set rate-limit headers
|
|
83
|
+
res.setHeader("X-RateLimit-Limit", String(this.maxRequests));
|
|
84
|
+
res.setHeader("X-RateLimit-Remaining", String(this.maxRequests - recent.length - 1));
|
|
85
|
+
res.setHeader("X-RateLimit-Reset", String(Math.ceil((now + this.windowMs) / 1000)));
|
|
86
|
+
|
|
87
|
+
next();
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Reset rate limit state for a specific key (useful in tests).
|
|
93
|
+
* @param {string} key
|
|
94
|
+
*/
|
|
95
|
+
resetKey(key) {
|
|
96
|
+
this.clients.delete(key);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reset all rate limit state.
|
|
101
|
+
*/
|
|
102
|
+
resetAll() {
|
|
103
|
+
this.clients.clear();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get current count for a key (useful in tests).
|
|
108
|
+
* @param {string} key
|
|
109
|
+
* @returns {number}
|
|
110
|
+
*/
|
|
111
|
+
getCount(key) {
|
|
112
|
+
const timestamps = this.clients.get(key);
|
|
113
|
+
if (!timestamps) return 0;
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
return timestamps.filter((t) => now - t < this.windowMs).length;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Dispose the limiter — clears the cleanup interval.
|
|
120
|
+
*/
|
|
121
|
+
dispose() {
|
|
122
|
+
if (this._cleanupInterval) {
|
|
123
|
+
clearInterval(this._cleanupInterval);
|
|
124
|
+
this._cleanupInterval = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Evict stale entries to prevent unbounded memory growth.
|
|
130
|
+
*/
|
|
131
|
+
_cleanup() {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const cutoff = now - this.windowMs;
|
|
134
|
+
for (const [key, timestamps] of this.clients.entries()) {
|
|
135
|
+
const recent = timestamps.filter((t) => t >= cutoff);
|
|
136
|
+
if (recent.length === 0) {
|
|
137
|
+
this.clients.delete(key);
|
|
138
|
+
} else {
|
|
139
|
+
this.clients.set(key, recent);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = RateLimiter;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A3M Router — Rate Limiter Tests
|
|
5
|
+
*
|
|
6
|
+
* Run with: node proxy/rate-limit.test.js
|
|
7
|
+
*
|
|
8
|
+
* Tests:
|
|
9
|
+
* 1. Allows requests under the limit
|
|
10
|
+
* 2. Blocks requests over the limit (429)
|
|
11
|
+
* 3. Returns consistent OpenAI-compatible error shape
|
|
12
|
+
* 4. Resets after the window expires
|
|
13
|
+
* 5. Multiple IPs are tracked independently
|
|
14
|
+
* 6. resetKey() clears state for a specific IP
|
|
15
|
+
* 7. resetAll() clears state for all IPs
|
|
16
|
+
* 8. X-RateLimit-* headers are set correctly
|
|
17
|
+
* 9. Environment variable configuration
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const assert = require("assert");
|
|
21
|
+
const http = require("http");
|
|
22
|
+
const express = require("express");
|
|
23
|
+
const path = require("path");
|
|
24
|
+
|
|
25
|
+
// Resolve the RateLimiter relative to this file's location
|
|
26
|
+
const RateLimiter = require(path.resolve(__dirname, "rate-limit"));
|
|
27
|
+
|
|
28
|
+
// ------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
let passed = 0;
|
|
33
|
+
let failed = 0;
|
|
34
|
+
|
|
35
|
+
function test(name, fn) {
|
|
36
|
+
try {
|
|
37
|
+
fn();
|
|
38
|
+
passed++;
|
|
39
|
+
console.log(` PASS ${name}`);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
failed++;
|
|
42
|
+
console.error(` FAIL ${name}`);
|
|
43
|
+
console.error(` ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a tiny test app with the rate limiter and return a helper
|
|
49
|
+
* that makes requests with a given IP.
|
|
50
|
+
*/
|
|
51
|
+
function createTestApp(limiter) {
|
|
52
|
+
const app = express();
|
|
53
|
+
app.use(limiter.middleware());
|
|
54
|
+
app.get("/test", (_req, res) => res.json({ ok: true }));
|
|
55
|
+
app.post("/v1/chat/completions", express.json(), (_req, res) =>
|
|
56
|
+
res.json({ choices: [{ message: { content: "ok" } }] })
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const server = app.listen(0); // random port
|
|
60
|
+
const port = server.address().port;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
server,
|
|
64
|
+
port,
|
|
65
|
+
/**
|
|
66
|
+
* Make a request and return { status, body, headers }.
|
|
67
|
+
*/
|
|
68
|
+
request: async (ip, method = "GET") => {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const req = http.request(
|
|
71
|
+
{
|
|
72
|
+
hostname: "127.0.0.1",
|
|
73
|
+
port,
|
|
74
|
+
path: method === "POST" ? "/v1/chat/completions" : "/test",
|
|
75
|
+
method,
|
|
76
|
+
headers: {
|
|
77
|
+
"X-Forwarded-For": ip,
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
(res) => {
|
|
82
|
+
let body = "";
|
|
83
|
+
res.on("data", (chunk) => (body += chunk));
|
|
84
|
+
res.on("end", () => {
|
|
85
|
+
try {
|
|
86
|
+
resolve({
|
|
87
|
+
status: res.statusCode,
|
|
88
|
+
body: JSON.parse(body),
|
|
89
|
+
headers: res.headers,
|
|
90
|
+
});
|
|
91
|
+
} catch {
|
|
92
|
+
resolve({ status: res.statusCode, body, headers: res.headers });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
if (method === "POST") {
|
|
98
|
+
req.write(JSON.stringify({ model: "a3m-auto", messages: [{ role: "user", content: "hi" }] }));
|
|
99
|
+
}
|
|
100
|
+
req.end();
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Wait for a given duration.
|
|
108
|
+
*/
|
|
109
|
+
function sleep(ms) {
|
|
110
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ------------------------------------------------------------------
|
|
114
|
+
// Tests
|
|
115
|
+
// ------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
async function run() {
|
|
118
|
+
console.log("\n RateLimiter Tests\n");
|
|
119
|
+
|
|
120
|
+
// --- 1. Allows requests under the limit ---
|
|
121
|
+
{
|
|
122
|
+
const limiter = new RateLimiter({ windowMs: 1000, maxRequests: 5 });
|
|
123
|
+
const app = createTestApp(limiter);
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < 5; i++) {
|
|
126
|
+
const result = await app.request("1.2.3.4");
|
|
127
|
+
assert.strictEqual(
|
|
128
|
+
result.status,
|
|
129
|
+
200,
|
|
130
|
+
`Request ${i + 1} should be allowed, got ${result.status}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
app.server.close();
|
|
135
|
+
limiter.dispose();
|
|
136
|
+
test("Allows requests under the limit", () => {});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- 2. Blocks requests over the limit (429) ---
|
|
140
|
+
{
|
|
141
|
+
const limiter = new RateLimiter({ windowMs: 1000, maxRequests: 3 });
|
|
142
|
+
const app = createTestApp(limiter);
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < 3; i++) {
|
|
145
|
+
await app.request("2.2.2.2");
|
|
146
|
+
}
|
|
147
|
+
// 4th request should be blocked
|
|
148
|
+
const blocked = await app.request("2.2.2.2");
|
|
149
|
+
assert.strictEqual(blocked.status, 429, "Expected 429 status");
|
|
150
|
+
|
|
151
|
+
app.server.close();
|
|
152
|
+
limiter.dispose();
|
|
153
|
+
test("Blocks requests over the limit (429)", () => {});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- 3. Returns consistent OpenAI-compatible error shape ---
|
|
157
|
+
{
|
|
158
|
+
const limiter = new RateLimiter({ windowMs: 1000, maxRequests: 1 });
|
|
159
|
+
const app = createTestApp(limiter);
|
|
160
|
+
|
|
161
|
+
await app.request("3.3.3.3");
|
|
162
|
+
const blocked = await app.request("3.3.3.3");
|
|
163
|
+
|
|
164
|
+
assert.strictEqual(blocked.status, 429);
|
|
165
|
+
assert.ok(blocked.body.error, "Response should have .error");
|
|
166
|
+
assert.strictEqual(blocked.body.error.type, "rate_limit_error");
|
|
167
|
+
assert.strictEqual(blocked.body.error.code, 429);
|
|
168
|
+
assert.ok(typeof blocked.body.error.retry_after === "number");
|
|
169
|
+
assert.ok(blocked.body.error.retry_after > 0);
|
|
170
|
+
|
|
171
|
+
app.server.close();
|
|
172
|
+
limiter.dispose();
|
|
173
|
+
test("Returns consistent OpenAI-compatible error shape", () => {});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- 4. Resets after the window expires ---
|
|
177
|
+
{
|
|
178
|
+
const limiter = new RateLimiter({ windowMs: 200, maxRequests: 1 });
|
|
179
|
+
const app = createTestApp(limiter);
|
|
180
|
+
|
|
181
|
+
await app.request("4.4.4.4");
|
|
182
|
+
const blocked = await app.request("4.4.4.4");
|
|
183
|
+
assert.strictEqual(blocked.status, 429, "Should be blocked initially");
|
|
184
|
+
|
|
185
|
+
// Wait for window to expire
|
|
186
|
+
await sleep(250);
|
|
187
|
+
|
|
188
|
+
const allowed = await app.request("4.4.4.4");
|
|
189
|
+
assert.strictEqual(allowed.status, 200, "Should be allowed after window expires");
|
|
190
|
+
|
|
191
|
+
app.server.close();
|
|
192
|
+
limiter.dispose();
|
|
193
|
+
test("Resets after the window expires", () => {});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- 5. Multiple IPs are tracked independently ---
|
|
197
|
+
{
|
|
198
|
+
const limiter = new RateLimiter({ windowMs: 1000, maxRequests: 2 });
|
|
199
|
+
const app = createTestApp(limiter);
|
|
200
|
+
|
|
201
|
+
// Exhaust ip-a
|
|
202
|
+
await app.request("ip-a");
|
|
203
|
+
await app.request("ip-a");
|
|
204
|
+
|
|
205
|
+
// ip-b should still be allowed
|
|
206
|
+
const b1 = await app.request("ip-b");
|
|
207
|
+
assert.strictEqual(b1.status, 200, "ip-b request 1 should be allowed");
|
|
208
|
+
const b2 = await app.request("ip-b");
|
|
209
|
+
assert.strictEqual(b2.status, 200, "ip-b request 2 should be allowed");
|
|
210
|
+
|
|
211
|
+
// ip-a should now be blocked
|
|
212
|
+
const a3 = await app.request("ip-a");
|
|
213
|
+
assert.strictEqual(a3.status, 429, "ip-a 3rd request should be blocked");
|
|
214
|
+
|
|
215
|
+
app.server.close();
|
|
216
|
+
limiter.dispose();
|
|
217
|
+
test("Multiple IPs are tracked independently", () => {});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- 6. resetKey() clears state for a specific IP ---
|
|
221
|
+
{
|
|
222
|
+
const limiter = new RateLimiter({ windowMs: 1000, maxRequests: 1 });
|
|
223
|
+
const app = createTestApp(limiter);
|
|
224
|
+
|
|
225
|
+
await app.request("6.6.6.6");
|
|
226
|
+
const blocked = await app.request("6.6.6.6");
|
|
227
|
+
assert.strictEqual(blocked.status, 429);
|
|
228
|
+
|
|
229
|
+
limiter.resetKey("6.6.6.6");
|
|
230
|
+
|
|
231
|
+
const allowed = await app.request("6.6.6.6");
|
|
232
|
+
assert.strictEqual(allowed.status, 200);
|
|
233
|
+
|
|
234
|
+
app.server.close();
|
|
235
|
+
limiter.dispose();
|
|
236
|
+
test("resetKey() clears state for a specific IP", () => {});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- 7. resetAll() clears state for all IPs ---
|
|
240
|
+
{
|
|
241
|
+
const limiter = new RateLimiter({ windowMs: 1000, maxRequests: 1 });
|
|
242
|
+
const app = createTestApp(limiter);
|
|
243
|
+
|
|
244
|
+
await app.request("7-a");
|
|
245
|
+
await app.request("7-b");
|
|
246
|
+
assert.strictEqual((await app.request("7-a")).status, 429);
|
|
247
|
+
assert.strictEqual((await app.request("7-b")).status, 429);
|
|
248
|
+
|
|
249
|
+
limiter.resetAll();
|
|
250
|
+
|
|
251
|
+
assert.strictEqual((await app.request("7-a")).status, 200);
|
|
252
|
+
assert.strictEqual((await app.request("7-b")).status, 200);
|
|
253
|
+
|
|
254
|
+
app.server.close();
|
|
255
|
+
limiter.dispose();
|
|
256
|
+
test("resetAll() clears state for all IPs", () => {});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- 8. X-RateLimit-* headers are set correctly ---
|
|
260
|
+
{
|
|
261
|
+
const limiter = new RateLimiter({ windowMs: 1000, maxRequests: 5 });
|
|
262
|
+
const app = createTestApp(limiter);
|
|
263
|
+
|
|
264
|
+
const result = await app.request("8.8.8.8");
|
|
265
|
+
assert.ok(result.headers["x-ratelimit-limit"], "Should have X-RateLimit-Limit");
|
|
266
|
+
assert.ok(result.headers["x-ratelimit-remaining"], "Should have X-RateLimit-Remaining");
|
|
267
|
+
assert.ok(result.headers["x-ratelimit-reset"], "Should have X-RateLimit-Reset");
|
|
268
|
+
assert.strictEqual(result.headers["x-ratelimit-limit"], "5");
|
|
269
|
+
|
|
270
|
+
// Exhaust and check blocked headers
|
|
271
|
+
for (let i = 0; i < 4; i++) await app.request("8.8.8.8");
|
|
272
|
+
|
|
273
|
+
const blocked = await app.request("8.8.8.8");
|
|
274
|
+
assert.strictEqual(blocked.status, 429);
|
|
275
|
+
assert.strictEqual(blocked.headers["x-ratelimit-remaining"], "0");
|
|
276
|
+
assert.ok(blocked.headers["retry-after"], "Should have Retry-After header");
|
|
277
|
+
|
|
278
|
+
app.server.close();
|
|
279
|
+
limiter.dispose();
|
|
280
|
+
test("X-RateLimit-* headers are set correctly", () => {});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --- 9. Rate limiter works with POST /v1/chat/completions ---
|
|
284
|
+
{
|
|
285
|
+
const limiter = new RateLimiter({ windowMs: 1000, maxRequests: 2 });
|
|
286
|
+
const app = createTestApp(limiter);
|
|
287
|
+
|
|
288
|
+
const r1 = await app.request("9.9.9.9", "POST");
|
|
289
|
+
assert.strictEqual(r1.status, 200);
|
|
290
|
+
|
|
291
|
+
const r2 = await app.request("9.9.9.9", "POST");
|
|
292
|
+
assert.strictEqual(r2.status, 200);
|
|
293
|
+
|
|
294
|
+
const r3 = await app.request("9.9.9.9", "POST");
|
|
295
|
+
assert.strictEqual(r3.status, 429);
|
|
296
|
+
|
|
297
|
+
app.server.close();
|
|
298
|
+
limiter.dispose();
|
|
299
|
+
test("Rate limiter works with POST /v1/chat/completions", () => {});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// --- Summary ---
|
|
303
|
+
const total = passed + failed;
|
|
304
|
+
console.log(`\n Results: ${passed}/${total} passed, ${failed}/${total} failed\n`);
|
|
305
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
run().catch((err) => {
|
|
309
|
+
console.error("FATAL:", err);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
});
|