adaptive-memory-multi-model-router 2.13.18 → 2.13.22

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.
Files changed (91) hide show
  1. package/.dockerignore +82 -0
  2. package/.env.example +303 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +83 -12
  4. package/.github/ISSUE_TEMPLATE/config.yml +12 -6
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +61 -10
  6. package/.github/PULL_REQUEST_TEMPLATE.md +53 -26
  7. package/.github/dependabot.yml +9 -0
  8. package/.github/workflows/codeql.yml +38 -0
  9. package/.github/workflows/npm-publish.yml +20 -0
  10. package/.github/workflows/stale.yml +56 -0
  11. package/ARCHITECTURE.md +346 -0
  12. package/AUDIT_REPORT.md +28 -0
  13. package/CHANGELOG.md +386 -22
  14. package/CONTRIBUTORS.md +20 -0
  15. package/Dockerfile +53 -0
  16. package/Dockerfile.proxy +33 -0
  17. package/PR_STATUS_REPORT.md +148 -0
  18. package/README.md +22 -0
  19. package/RUNKIT.md +83 -0
  20. package/_schema.html +61 -15
  21. package/articles/AI_AGENT_LLM_ROUTING.md +150 -0
  22. package/articles/FROM_ZERO_TO_10K.md +107 -0
  23. package/articles/LLM_BENCHMARK_DEEP_DIVE.md +153 -0
  24. package/articles/TWEETS_10K_DOWNLOADS.md +47 -0
  25. package/articles/TWEETS_BENCHMARK_FIRST.md +46 -0
  26. package/articles/TWEETS_MCP_PLAY.md +51 -0
  27. package/articles/TWEETS_SEQUENTIAL_BROKEN.md +49 -0
  28. package/articles/TWEETS_WHY_BUILD.md +54 -0
  29. package/benchmark-results.json +26 -45
  30. package/cli/a3m +840 -0
  31. package/demo/package.json +13 -0
  32. package/demo/public/index.html +762 -0
  33. package/demo/server.js +405 -0
  34. package/dist/cli.js +4 -0
  35. package/docker-compose.yml +74 -0
  36. package/docs/.nojekyll +0 -0
  37. package/docs/BENCHMARK.md +96 -22
  38. package/docs/_config.yml +49 -0
  39. package/docs/api.html +513 -0
  40. package/docs/benchmark.html +387 -0
  41. package/docs/cli-cheatsheet.md +339 -0
  42. package/docs/comparison.md +108 -0
  43. package/docs/curl-examples.md +247 -0
  44. package/docs/index.html +390 -99
  45. package/docs/openapi.yaml +1318 -0
  46. package/docs/quick-start.html +366 -0
  47. package/docs/robots.txt +1 -1
  48. package/docs/sitemap.xml +23 -5
  49. package/docs/styles.css +682 -0
  50. package/examples/README.md +61 -0
  51. package/examples/a3m-sdk.js +124 -0
  52. package/examples/basic-route.js +54 -0
  53. package/examples/chat-loop.js +202 -0
  54. package/examples/classify-then-route.js +102 -0
  55. package/examples/cost-compare.js +120 -0
  56. package/examples/ensemble.js +160 -0
  57. package/integrations/langchain/README.md +216 -0
  58. package/integrations/langchain/a3m_langchain.ts +1360 -0
  59. package/integrations/langchain/example.ts +287 -0
  60. package/integrations/vercel-ai-sdk/README.md +49 -0
  61. package/integrations/vercel-ai-sdk/a3m_provider.ts +78 -0
  62. package/integrations/vercel-ai-sdk/example.ts +25 -0
  63. package/llms-full.txt +43 -0
  64. package/llms.txt +9 -0
  65. package/mcp-server/README.md +188 -0
  66. package/mcp-server/package.json +29 -0
  67. package/mcp-server/src/index.ts +744 -0
  68. package/mcp-server/tsconfig.json +19 -0
  69. package/package.json +3 -3
  70. package/proxy/README.md +227 -0
  71. package/proxy/package-lock.json +831 -0
  72. package/proxy/package.json +17 -0
  73. package/proxy/rate-limit.js +145 -0
  74. package/proxy/rate-limit.test.js +311 -0
  75. package/proxy/server.js +970 -0
  76. package/scripts/banner.js +29 -0
  77. package/scripts/compare-providers.sh +230 -0
  78. package/scripts/cross_post.py +443 -0
  79. package/scripts/publish_fcc.py +106 -0
  80. package/scripts/push-to-gitee.sh +52 -0
  81. package/src/tui/dashboard.ts +13 -0
  82. package/tests/__mocks__/tokenUtils.ts +22 -0
  83. package/tests/memory/episodicMemory.test.ts +227 -0
  84. package/tests/package-lock.json +1628 -0
  85. package/tests/package.json +18 -0
  86. package/tests/routing/ensembleVoting.test.ts +236 -0
  87. package/tests/routing/providerRetry.test.ts +360 -0
  88. package/tests/routing/queryTypePresets.test.ts +206 -0
  89. package/tests/tsconfig.json +21 -0
  90. package/tests/vitest.config.ts +18 -0
  91. 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
+ });