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.
- 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
package/demo/server.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3M Router Demo Server
|
|
3
|
+
*
|
|
4
|
+
* Interactive web demo showing parallel multi-LLM routing.
|
|
5
|
+
* Works without API keys — falls back to mock results.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import express from "express";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
// ============================================================
|
|
15
|
+
// Lazy-load A3M Router (fail gracefully if not installed locally)
|
|
16
|
+
// ============================================================
|
|
17
|
+
let a3m;
|
|
18
|
+
try {
|
|
19
|
+
a3m = await import("adaptive-memory-multi-model-router");
|
|
20
|
+
} catch {
|
|
21
|
+
console.warn("[demo] A3M Router not installed locally — using file:// import");
|
|
22
|
+
// Try relative path for development
|
|
23
|
+
try {
|
|
24
|
+
a3m = await import("../dist/index.js");
|
|
25
|
+
} catch {
|
|
26
|
+
a3m = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ============================================================
|
|
31
|
+
// Fallback responses for when no real API keys are configured
|
|
32
|
+
// ============================================================
|
|
33
|
+
const MOCK_RESPONSES = {
|
|
34
|
+
"Write a haiku about Kubernetes": {
|
|
35
|
+
content: `Pods drift through the cloud\nOrchestrator hums softly\nContainers at rest`,
|
|
36
|
+
model: "groq/llama-3.3-70b-versatile",
|
|
37
|
+
provider: "Groq",
|
|
38
|
+
},
|
|
39
|
+
"Explain quantum computing to a 5-year-old": {
|
|
40
|
+
content: `Imagine you have a magical coin. Normal coins are either heads OR tails. But a magical quantum coin can be heads AND tails at the same time! That's what quantum computers use — they have special "magic coins" called qubits (say "kyoo-bits").
|
|
41
|
+
|
|
42
|
+
While a regular computer checks one answer at a time (like trying every key on a keychain), a quantum computer can check ALL the answers at once. That makes it super fast at solving certain puzzles, like finding the right key instantly!`,
|
|
43
|
+
model: "anthropic/claude-sonnet-4-20250514",
|
|
44
|
+
provider: "Anthropic",
|
|
45
|
+
},
|
|
46
|
+
"Write a React component for a search bar": {
|
|
47
|
+
content: `\`\`\`jsx
|
|
48
|
+
import React, { useState, useCallback } from 'react';
|
|
49
|
+
|
|
50
|
+
function SearchBar({ onSearch, placeholder = "Search..." }) {
|
|
51
|
+
const [query, setQuery] = useState("");
|
|
52
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
53
|
+
|
|
54
|
+
const handleChange = useCallback((e) => {
|
|
55
|
+
setQuery(e.target.value);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const handleSubmit = useCallback((e) => {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
if (query.trim()) onSearch(query.trim());
|
|
61
|
+
}, [query, onSearch]);
|
|
62
|
+
|
|
63
|
+
const handleClear = useCallback(() => {
|
|
64
|
+
setQuery("");
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<form
|
|
69
|
+
onSubmit={handleSubmit}
|
|
70
|
+
style={{
|
|
71
|
+
|
|
72
|
+
display: "flex",
|
|
73
|
+
alignItems: "center",
|
|
74
|
+
background: isFocused ? "#fff" : "#f5f5f5",
|
|
75
|
+
border: \`2px solid \${isFocused ? "#6366f1" : "#e5e7eb"}\`,
|
|
76
|
+
borderRadius: "12px",
|
|
77
|
+
padding: "8px 16px",
|
|
78
|
+
transition: "all 0.2s ease",
|
|
79
|
+
maxWidth: "500px",
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" style={{ marginRight: 8 }}>
|
|
83
|
+
<path d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" fill="#9ca3af"/>
|
|
84
|
+
</svg>
|
|
85
|
+
<input
|
|
86
|
+
type="text"
|
|
87
|
+
value={query}
|
|
88
|
+
onChange={handleChange}
|
|
89
|
+
onFocus={() => setIsFocused(true)}
|
|
90
|
+
onBlur={() => setIsFocused(false)}
|
|
91
|
+
placeholder={placeholder}
|
|
92
|
+
style={{
|
|
93
|
+
flex: 1,
|
|
94
|
+
border: "none",
|
|
95
|
+
outline: "none",
|
|
96
|
+
background: "transparent",
|
|
97
|
+
fontSize: "16px",
|
|
98
|
+
padding: "4px 0",
|
|
99
|
+
color: "#1f2937",
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
{query && (
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={handleClear}
|
|
106
|
+
style={{
|
|
107
|
+
background: "none",
|
|
108
|
+
border: "none",
|
|
109
|
+
cursor: "pointer",
|
|
110
|
+
padding: 4,
|
|
111
|
+
color: "#9ca3af",
|
|
112
|
+
}}
|
|
113
|
+
aria-label="Clear search"
|
|
114
|
+
>
|
|
115
|
+
✕
|
|
116
|
+
</button>
|
|
117
|
+
)}
|
|
118
|
+
<button
|
|
119
|
+
type="submit"
|
|
120
|
+
style={{
|
|
121
|
+
background: "#6366f1",
|
|
122
|
+
color: "#fff",
|
|
123
|
+
border: "none",
|
|
124
|
+
borderRadius: "8px",
|
|
125
|
+
padding: "8px 16px",
|
|
126
|
+
marginLeft: 8,
|
|
127
|
+
cursor: "pointer",
|
|
128
|
+
fontWeight: 600,
|
|
129
|
+
fontSize: "14px",
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
Search
|
|
133
|
+
</button>
|
|
134
|
+
</form>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default SearchBar;
|
|
139
|
+
\`\`\``,
|
|
140
|
+
model: "openai/gpt-4o",
|
|
141
|
+
provider: "OpenAI",
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const DEFAULT_MOCK = {
|
|
146
|
+
content:
|
|
147
|
+
"A3M Router selected the optimal model for your query. Configure API keys to get real LLM responses — the routing engine analyzed your query's complexity, domain, and requirements to find the best provider.",
|
|
148
|
+
model: "a3m-routing/auto",
|
|
149
|
+
provider: "A3M Routing Engine",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// ============================================================
|
|
153
|
+
// Cost calculator (based on A3M Router token costs)
|
|
154
|
+
// ============================================================
|
|
155
|
+
const MODEL_COSTS = {
|
|
156
|
+
"groq/llama-3.3-70b-versatile": { input: 0.59, output: 0.79 },
|
|
157
|
+
"anthropic/claude-sonnet-4-20250514": { input: 3.0, output: 15.0 },
|
|
158
|
+
"openai/gpt-4o": { input: 2.5, output: 10.0 },
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function estimateCost(model, content) {
|
|
162
|
+
const tokens = Math.ceil(content.length / 4); // rough estimate
|
|
163
|
+
const costs = MODEL_COSTS[model] || { input: 0.5, output: 0.5 };
|
|
164
|
+
return ((tokens / 1_000_000) * (costs.input + costs.output)).toFixed(6);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================
|
|
168
|
+
// Express App
|
|
169
|
+
// ============================================================
|
|
170
|
+
const app = express();
|
|
171
|
+
app.use(express.json());
|
|
172
|
+
app.use(express.static(path.join(__dirname, "public")));
|
|
173
|
+
|
|
174
|
+
// ── GET /api/providers ───────────────────────────────────
|
|
175
|
+
app.get("/api/providers", (req, res) => {
|
|
176
|
+
let providers = {};
|
|
177
|
+
let hasRealKeys = false;
|
|
178
|
+
|
|
179
|
+
if (a3m) {
|
|
180
|
+
try {
|
|
181
|
+
providers = a3m.getAvailableProviders();
|
|
182
|
+
hasRealKeys = Object.keys(providers).length > 0;
|
|
183
|
+
} catch {
|
|
184
|
+
providers = {};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Build a clean list of what's available
|
|
189
|
+
const list = Object.entries(providers).map(([id, p]) => ({
|
|
190
|
+
id,
|
|
191
|
+
name: p.name,
|
|
192
|
+
tier: p.tier,
|
|
193
|
+
models: p.models?.length || 0,
|
|
194
|
+
type: p.type,
|
|
195
|
+
costPerM: p.costPerK
|
|
196
|
+
? { input: p.costPerK.input, output: p.costPerK.output }
|
|
197
|
+
: { input: 0, output: 0 },
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
res.json({
|
|
201
|
+
count: list.length,
|
|
202
|
+
hasApiKeys: hasRealKeys,
|
|
203
|
+
providers: list,
|
|
204
|
+
note: hasRealKeys
|
|
205
|
+
? "API keys detected — real LLM responses enabled"
|
|
206
|
+
: "No API keys configured — showing simulated responses",
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── POST /api/analyze ───────────────────────────────────
|
|
211
|
+
app.post("/api/analyze", (req, res) => {
|
|
212
|
+
const { prompt } = req.body;
|
|
213
|
+
if (!prompt || typeof prompt !== "string") {
|
|
214
|
+
return res.status(400).json({ error: "prompt is required" });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let features = null;
|
|
218
|
+
let routing = null;
|
|
219
|
+
|
|
220
|
+
if (a3m) {
|
|
221
|
+
try {
|
|
222
|
+
features = a3m.extractQueryFeatures(prompt);
|
|
223
|
+
routing = a3m.routeQuery(prompt);
|
|
224
|
+
} catch {
|
|
225
|
+
// fallback below
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!features) {
|
|
230
|
+
const complexity = Math.min(1, 0.15 + prompt.length / 500);
|
|
231
|
+
features = {
|
|
232
|
+
complexity,
|
|
233
|
+
length: prompt.split(/\s+/).length,
|
|
234
|
+
has_code: /function|class |def |import|<[a-z]+|const |let |var /i.test(prompt),
|
|
235
|
+
has_math: /\d+[\+\-\*\/=]|calculate|sum|equation/i.test(prompt),
|
|
236
|
+
requires_reasoning: /why|explain|analyze|compare|reason/i.test(prompt),
|
|
237
|
+
is_creative: /write|story|poem|create|imagine/i.test(prompt),
|
|
238
|
+
detected_domain: "general",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!routing) {
|
|
243
|
+
routing = {
|
|
244
|
+
primary_model: "a3m/auto-routed",
|
|
245
|
+
confidence: 0.85,
|
|
246
|
+
reasoning: "Intelligent routing based on query complexity and domain",
|
|
247
|
+
estimated_cost: "0.000001",
|
|
248
|
+
estimated_latency_ms: 420,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
res.json({ features, routing });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ── POST /api/route ────────────────────────────────────
|
|
256
|
+
app.post("/api/route", async (req, res) => {
|
|
257
|
+
const { prompt, mode } = req.body;
|
|
258
|
+
if (!prompt || typeof prompt !== "string") {
|
|
259
|
+
return res.status(400).json({ error: "prompt is required" });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const startTime = Date.now();
|
|
263
|
+
const isEnsemble = mode === "ensemble";
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
// 1) Route the query using A3M Router
|
|
267
|
+
let routing = null;
|
|
268
|
+
let features = null;
|
|
269
|
+
|
|
270
|
+
if (a3m) {
|
|
271
|
+
try {
|
|
272
|
+
routing = a3m.routeQuery(prompt);
|
|
273
|
+
features = a3m.extractQueryFeatures(prompt);
|
|
274
|
+
} catch {
|
|
275
|
+
// fallback
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 2) Try real LLM execution via proxy server
|
|
280
|
+
let content = null;
|
|
281
|
+
let provider = null;
|
|
282
|
+
let model = null;
|
|
283
|
+
let usedRealProvider = false;
|
|
284
|
+
|
|
285
|
+
// Check if we have any API keys configured
|
|
286
|
+
let availableProviders = {};
|
|
287
|
+
if (a3m) {
|
|
288
|
+
try {
|
|
289
|
+
availableProviders = a3m.getAvailableProviders();
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const hasRealKeys = Object.keys(availableProviders).length > 0;
|
|
294
|
+
|
|
295
|
+
if (hasRealKeys && a3m) {
|
|
296
|
+
try {
|
|
297
|
+
// Try routing via proxy
|
|
298
|
+
const modelName = "auto";
|
|
299
|
+
const messages = [{ role: "user", content: prompt }];
|
|
300
|
+
|
|
301
|
+
const modelMapper = await import("../dist/server/modelMapper.js");
|
|
302
|
+
const mapping = modelMapper.resolveModel(modelName, prompt);
|
|
303
|
+
if (mapping && mapping.apiKey) {
|
|
304
|
+
const baseUrl = mapping.baseUrl;
|
|
305
|
+
const body = {
|
|
306
|
+
model: mapping.model,
|
|
307
|
+
messages,
|
|
308
|
+
max_tokens: 512,
|
|
309
|
+
};
|
|
310
|
+
const headers = { "Content-Type": "application/json" };
|
|
311
|
+
if (mapping.apiKey) headers["Authorization"] = `Bearer ${mapping.apiKey}`;
|
|
312
|
+
|
|
313
|
+
const resp = await fetch(baseUrl, {
|
|
314
|
+
method: "POST",
|
|
315
|
+
headers,
|
|
316
|
+
body: JSON.stringify(body),
|
|
317
|
+
});
|
|
318
|
+
const data = await resp.json();
|
|
319
|
+
if (data.choices?.[0]?.message?.content) {
|
|
320
|
+
content = data.choices[0].message.content;
|
|
321
|
+
provider = mapping.providerId;
|
|
322
|
+
model = mapping.model;
|
|
323
|
+
usedRealProvider = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.warn("[demo] Real provider call failed:", e.message);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 3) Fallback to mock if no real response
|
|
332
|
+
if (!content) {
|
|
333
|
+
const mockKey = Object.keys(MOCK_RESPONSES).find((k) => prompt.includes(k));
|
|
334
|
+
const mock = mockKey ? MOCK_RESPONSES[mockKey] : DEFAULT_MOCK;
|
|
335
|
+
content = mock.content;
|
|
336
|
+
provider = mock.provider;
|
|
337
|
+
model = mock.model;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 4) Ensemble mode: return multiple responses
|
|
341
|
+
let responses = null;
|
|
342
|
+
if (isEnsemble) {
|
|
343
|
+
const providers = ["Groq", "Anthropic", "OpenAI"];
|
|
344
|
+
responses = providers.map((p) => ({
|
|
345
|
+
provider: p,
|
|
346
|
+
model: p === "Groq"
|
|
347
|
+
? "llama-3.3-70b-versatile"
|
|
348
|
+
: p === "Anthropic"
|
|
349
|
+
? "claude-sonnet-4-20250514"
|
|
350
|
+
: "gpt-4o",
|
|
351
|
+
content: `[Simulated response from ${p}] A3M Router selected ${p} for optimal performance on your query. Configure API keys to get real responses from all providers simultaneously.`,
|
|
352
|
+
latency: Math.floor(Math.random() * 400 + 200),
|
|
353
|
+
cost: (Math.random() * 0.002).toFixed(6),
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const latency = Date.now() - startTime;
|
|
358
|
+
const cost = usedRealProvider
|
|
359
|
+
? estimateCost(model, content)
|
|
360
|
+
: "0.000001";
|
|
361
|
+
|
|
362
|
+
res.json({
|
|
363
|
+
result: content,
|
|
364
|
+
provider,
|
|
365
|
+
model,
|
|
366
|
+
mode: isEnsemble ? "ensemble" : "single",
|
|
367
|
+
latency,
|
|
368
|
+
cost,
|
|
369
|
+
usedMock: !usedRealProvider,
|
|
370
|
+
features,
|
|
371
|
+
routing,
|
|
372
|
+
responses,
|
|
373
|
+
hasRealKeys,
|
|
374
|
+
});
|
|
375
|
+
} catch (err) {
|
|
376
|
+
console.error("[demo] Route error:", err);
|
|
377
|
+
res.status(500).json({
|
|
378
|
+
error: err.message,
|
|
379
|
+
result: "A3M Router encountered an error processing your query.",
|
|
380
|
+
provider: "A3M Router",
|
|
381
|
+
model: "error",
|
|
382
|
+
latency: Date.now() - startTime,
|
|
383
|
+
cost: "0",
|
|
384
|
+
usedMock: true,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// ── Serve index.html for all other routes ──────────────
|
|
390
|
+
app.get("*", (req, res) => {
|
|
391
|
+
res.sendFile(path.join(__dirname, "public", "index.html"));
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ── Start ──────────────────────────────────────────────
|
|
395
|
+
const PORT = process.env.PORT || 3000;
|
|
396
|
+
app.listen(PORT, () => {
|
|
397
|
+
console.log(`
|
|
398
|
+
A3M Router Demo Server
|
|
399
|
+
─────────────────────────
|
|
400
|
+
Running: http://localhost:${PORT}
|
|
401
|
+
API Key: ${process.env.OPENAI_API_KEY ? "Yes" : "No (mock mode)"}
|
|
402
|
+
Providers: ${a3m ? "A3M SDK loaded" : "A3M SDK not available"}
|
|
403
|
+
Mode: ${process.env.OPENAI_API_KEY || process.env.GROQ_API_KEY ? "Live" : "Demo (mock responses)"}
|
|
404
|
+
`);
|
|
405
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -154,6 +154,10 @@ async function callProvider(providerId, model, prompt, maxTokens) {
|
|
|
154
154
|
// ============================================================
|
|
155
155
|
|
|
156
156
|
async function main() {
|
|
157
|
+
// Show banner
|
|
158
|
+
const banner = require('./scripts/banner.js');
|
|
159
|
+
process.stdout.write(banner);
|
|
160
|
+
|
|
157
161
|
const router = createA3MRouter({ memory: { maxSize: 1000 } });
|
|
158
162
|
|
|
159
163
|
switch (command) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# A3M Router — Docker Compose
|
|
3
|
+
# Runs the main router service + OpenAI-compatible proxy side-by-side
|
|
4
|
+
# =============================================================================
|
|
5
|
+
|
|
6
|
+
version: "3.9"
|
|
7
|
+
|
|
8
|
+
services:
|
|
9
|
+
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
# A3M Router — Main API & health endpoint
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
a3m-router:
|
|
14
|
+
build:
|
|
15
|
+
context: .
|
|
16
|
+
dockerfile: Dockerfile
|
|
17
|
+
container_name: a3m-router
|
|
18
|
+
restart: unless-stopped
|
|
19
|
+
ports:
|
|
20
|
+
- "3000:3000"
|
|
21
|
+
environment:
|
|
22
|
+
- NODE_ENV=production
|
|
23
|
+
- PORT=3000
|
|
24
|
+
# Pass through any .env variables needed at runtime
|
|
25
|
+
- NVIDIA_API_KEY=${NVIDIA_API_KEY}
|
|
26
|
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
|
27
|
+
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
|
28
|
+
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
|
29
|
+
- GROQ_API_KEY=${GROQ_API_KEY}
|
|
30
|
+
- CEREBRAS_API_KEY=${CEREBRAS_API_KEY}
|
|
31
|
+
# Add additional provider keys as needed
|
|
32
|
+
env_file:
|
|
33
|
+
- .env
|
|
34
|
+
healthcheck:
|
|
35
|
+
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
|
|
36
|
+
interval: 30s
|
|
37
|
+
timeout: 5s
|
|
38
|
+
start_period: 10s
|
|
39
|
+
retries: 3
|
|
40
|
+
logging:
|
|
41
|
+
driver: "json-file"
|
|
42
|
+
options:
|
|
43
|
+
max-size: "10m"
|
|
44
|
+
max-file: "3"
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# A3M Proxy — OpenAI-compatible proxy server (port 8787)
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
a3m-proxy:
|
|
50
|
+
build:
|
|
51
|
+
context: .
|
|
52
|
+
dockerfile: Dockerfile.proxy
|
|
53
|
+
container_name: a3m-proxy
|
|
54
|
+
restart: unless-stopped
|
|
55
|
+
ports:
|
|
56
|
+
- "8787:8787"
|
|
57
|
+
environment:
|
|
58
|
+
- NODE_ENV=production
|
|
59
|
+
- PORT=8787
|
|
60
|
+
- ROUTER_BASE_URL=http://a3m-router:3000
|
|
61
|
+
depends_on:
|
|
62
|
+
a3m-router:
|
|
63
|
+
condition: service_healthy
|
|
64
|
+
healthcheck:
|
|
65
|
+
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8787/health"]
|
|
66
|
+
interval: 30s
|
|
67
|
+
timeout: 5s
|
|
68
|
+
start_period: 10s
|
|
69
|
+
retries: 3
|
|
70
|
+
logging:
|
|
71
|
+
driver: "json-file"
|
|
72
|
+
options:
|
|
73
|
+
max-size: "10m"
|
|
74
|
+
max-file: "3"
|
package/docs/.nojekyll
ADDED
|
File without changes
|
package/docs/BENCHMARK.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# A3M Router — Independent Benchmark
|
|
2
2
|
|
|
3
|
+
A3M Router is benchmarked on two independent dimensions:
|
|
4
|
+
|
|
5
|
+
1. **Latency** — How much overhead does the gateway add? (real API calls)
|
|
6
|
+
2. **Routing Accuracy** — How well does the complexity classifier sort queries into tiers? (offline, 200 queries)
|
|
7
|
+
|
|
8
|
+
Both benchmarks are reproducible — scripts live in `scripts/`.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Latency Benchmark
|
|
13
|
+
|
|
3
14
|
**The question everyone asks:** *"How much latency does a gateway add?"*
|
|
4
15
|
|
|
5
16
|
**The answer:** +96ms for passthrough, +236ms for full intelligent routing — on a 138ms baseline.
|
|
@@ -8,9 +19,7 @@
|
|
|
8
19
|
|
|
9
20
|
*Left: latency comparison. Right: cost savings projection. Dark theme.*
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## The TL;DR
|
|
22
|
+
### The TL;DR
|
|
14
23
|
|
|
15
24
|
```
|
|
16
25
|
Direct call to Groq: ──▸ 138ms (baseline)
|
|
@@ -25,9 +34,7 @@ Through A3M auto (routed): ──▸ 374ms (+140ms = routing decision)
|
|
|
25
34
|
|
|
26
35
|
**Total overhead: 236ms.** Less than the time it takes to blink.
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## The Details
|
|
37
|
+
### The Details
|
|
31
38
|
|
|
32
39
|
| Scenario | Time | What's happening |
|
|
33
40
|
|:---------|:----:|:-----------------|
|
|
@@ -35,11 +42,9 @@ Through A3M auto (routed): ──▸ 374ms (+140ms = routing decision)
|
|
|
35
42
|
| **Through A3M (forced route)** | **234ms** | Request hits A3M proxy. Guardrails scan for prompt injection (17 patterns) and PII. Cache checks for semantic duplicates. Cost tracker logs the call. Request forwarded to Groq. Response logged. |
|
|
36
43
|
| **Through A3M (auto route)** | **374ms** | Everything above, plus: A3M's router extracts 12 signals from the query text — domain, task type, complexity, verb intensity, multi-step structure. Scores it. Assigns a tier. Selects the cheapest capable model. Forwards the request. |
|
|
37
44
|
|
|
38
|
-
**The extra 140ms for auto-routing is the intelligence.**
|
|
39
|
-
|
|
40
|
-
---
|
|
45
|
+
**The extra 140ms for auto-routing is the intelligence.**
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
### The Trade-Off
|
|
43
48
|
|
|
44
49
|
```text
|
|
45
50
|
Without A3M With A3M
|
|
@@ -54,9 +59,7 @@ Cost visibility: End-of-month surprise Per-query tracking + budget
|
|
|
54
59
|
|
|
55
60
|
**236ms of overhead saves you $2,604/year.** That's about $11 per millisecond.
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
## Why Most Gateways Don't Publish This
|
|
62
|
+
### Why Most Gateways Don't Publish This
|
|
60
63
|
|
|
61
64
|
Every gateway adds latency. Most don't publish their numbers because they're either:
|
|
62
65
|
|
|
@@ -66,23 +69,15 @@ Every gateway adds latency. Most don't publish their numbers because they're eit
|
|
|
66
69
|
|
|
67
70
|
A3M publishes this because the numbers are honest and the trade-off is clear: **pay 236ms, save 62%, get production-grade security.**
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
## Reproduce This Yourself
|
|
72
|
+
### Reproduce This
|
|
72
73
|
|
|
73
74
|
```bash
|
|
74
|
-
# Install the benchmark tool
|
|
75
75
|
pip install llm-gateway-bench
|
|
76
|
-
|
|
77
|
-
# Start A3M proxy
|
|
78
76
|
npx a3m-router serve
|
|
79
|
-
|
|
80
|
-
# Run comparison
|
|
81
77
|
python3 -m llm_gateway_bench.cli run groq \
|
|
82
78
|
--model llama-3.3-70b-versatile \
|
|
83
79
|
--prompt "What is the capital of France?" \
|
|
84
80
|
--requests 10
|
|
85
|
-
|
|
86
81
|
python3 -m llm_gateway_bench.cli run custom \
|
|
87
82
|
--model auto \
|
|
88
83
|
--base-url http://localhost:8787/v1 \
|
|
@@ -94,3 +89,82 @@ python3 -m llm_gateway_bench.cli run custom \
|
|
|
94
89
|
**Run date:** 2026-05-26
|
|
95
90
|
**Provider:** Groq (llama-3.3-70b-versatile)
|
|
96
91
|
**Methodology:** 3 prompts × 5 requests = 15 calls per scenario, real API calls
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 2. Routing Accuracy Benchmark
|
|
96
|
+
|
|
97
|
+
**The question everyone asks:** *"Does the complexity classifier actually pick the right tier?"*
|
|
98
|
+
|
|
99
|
+
**The answer:** **99.5% ±1 tier accuracy** across 200 diverse queries — no ML training needed.
|
|
100
|
+
|
|
101
|
+
Benchmark script: `scripts/routing-benchmark-v2.js`
|
|
102
|
+
Methodology: RouteLLM-inspired (arXiv:2404.06035), 4-tier classification
|
|
103
|
+
|
|
104
|
+
### Results (2026-05-28)
|
|
105
|
+
|
|
106
|
+
| Metric | Score | What It Means |
|
|
107
|
+
|:-------|:-----:|:--------------|
|
|
108
|
+
| **±1 Tier Accuracy** | **99.5%** | Only 1 in 200 queries is misrouted by >1 tier |
|
|
109
|
+
| Exact Tier Match | 64.5% | ~2 in 3 queries hit the *exact* right tier |
|
|
110
|
+
| Free Tier Recall | 92.0% | Simple queries correctly routed to $0 models |
|
|
111
|
+
| Cheap Tier Recall | 78.3% | Standard code/translation routed to cheap |
|
|
112
|
+
| Mid Tier Recall | 36.0% | Complex reasoning often routed cheaper (fallback-safe) |
|
|
113
|
+
| Premium Tier Recall | 45.0% | Expert queries routed to premium |
|
|
114
|
+
| Over-routing (waste) | 7.0% | Sent to a stronger but costlier model than needed |
|
|
115
|
+
| Under-routing (risk) | 28.5% | Sent weak first; auto-fallback in <2s |
|
|
116
|
+
| Cost Savings vs All-Premium | **61.6%** | At 100K queries/mo: **save $77.04/mo** |
|
|
117
|
+
|
|
118
|
+
### Confusion Matrix
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
Expected \\ Routed free cheap mid premium
|
|
122
|
+
──────────────────────────────────────────────────
|
|
123
|
+
free 46✓ 4 0 0
|
|
124
|
+
cheap 11 47✓ 2 0
|
|
125
|
+
mid 0 24 18✓ 8
|
|
126
|
+
premium 0 1 21 18✓
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Complexity Score Distribution
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
free avg=0.125 range=[0.100, 0.270]
|
|
133
|
+
cheap avg=0.275 range=[0.100, 0.575]
|
|
134
|
+
mid avg=0.477 range=[0.230, 0.710]
|
|
135
|
+
premium avg=0.690 range=[0.430, 1.000]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Test Set
|
|
139
|
+
|
|
140
|
+
- **50 simple** — trivia, basic math, yes/no (target: free)
|
|
141
|
+
- **60 medium** — code snippets, summarization, translation (target: cheap)
|
|
142
|
+
- **50 complex** — reasoning, analysis, system design (target: mid)
|
|
143
|
+
- **40 expert** — legal, medical, security, finance (target: premium)
|
|
144
|
+
|
|
145
|
+
### Third-Party Cross-Validation
|
|
146
|
+
|
|
147
|
+
A3M's tier assignments align with **MMLU accuracy rankings**:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
Provider MMLU A3M Tier Source
|
|
151
|
+
────────────────────────────────────────────────
|
|
152
|
+
gpt-4o 88.7% premium MMLU Leaderboard
|
|
153
|
+
claude-3.5-sonnet 88.4% premium MMLU Leaderboard
|
|
154
|
+
gemini-1.5-pro 85.7% premium MMLU Leaderboard
|
|
155
|
+
mistral-large 84.2% mid MMLU Leaderboard
|
|
156
|
+
llama-3.3-70b 82.5% mid MMLU Leaderboard
|
|
157
|
+
deepseek-v2 78.3% mid MMLU Leaderboard
|
|
158
|
+
llama-3.1-8b 68.3% cheap MMLU Leaderboard
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**References:** [MMLU Leaderboard](https://paperswithcode.com/sota/multi-task-language-understanding-on-mmlu), [RouteLLM arXiv:2404.06035](https://arxiv.org/abs/2404.06035)
|
|
162
|
+
|
|
163
|
+
### Reproduce This
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
cd /path/to/a3m-router
|
|
167
|
+
node scripts/routing-benchmark-v2.js
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Outputs `benchmark-results.json` with full breakdown.
|