adaptive-memory-multi-model-router 1.9.5 → 2.0.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.
Files changed (121) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +37 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +44 -0
  5. package/.github/workflows/npm-stats-validation.yml +152 -0
  6. package/.github/workflows/pages.yml +37 -0
  7. package/CHANGELOG.md +122 -0
  8. package/CODE_OF_CONDUCT.md +128 -0
  9. package/CONTRIBUTING.md +110 -0
  10. package/LAUNCH-PAIN-DRIVEN.md +339 -0
  11. package/LAUNCH.md +575 -0
  12. package/POPULARITY_BOOSTERS.md +285 -0
  13. package/README.md +231 -300
  14. package/SECURITY.md +69 -0
  15. package/articles/CONTENT_STRUCTURE.md +292 -0
  16. package/articles/DEVTO_COST_GUIDE.md +473 -0
  17. package/articles/DEVTO_FINAL.md +416 -0
  18. package/articles/DEVTO_MULTI_PROVIDER.md +542 -0
  19. package/articles/HN_10X_BETTER.md +430 -0
  20. package/articles/HN_CHINESE_STYLE.md +308 -0
  21. package/articles/HN_FINAL.md +199 -0
  22. package/articles/HN_POSTED_VERSION.md +56 -0
  23. package/articles/HN_RESEARCH.md +364 -0
  24. package/articles/PAIN-DRIVEN-devto-v2.md +308 -0
  25. package/articles/PAIN-DRIVEN-devto-v3.md +268 -0
  26. package/articles/PAIN-DRIVEN-devto.md +242 -0
  27. package/articles/PAIN-DRIVEN-hackernews-v2.md +138 -0
  28. package/articles/PAIN-DRIVEN-hackernews-v3.md +151 -0
  29. package/articles/PAIN-DRIVEN-hackernews.md +131 -0
  30. package/articles/PAIN-DRIVEN-reddit-v2.md +301 -0
  31. package/articles/PAIN-DRIVEN-reddit-v3.md +236 -0
  32. package/articles/PAIN-DRIVEN-reddit.md +218 -0
  33. package/articles/PAIN-DRIVEN-twitter-v2.md +110 -0
  34. package/articles/PAIN-DRIVEN-twitter-v3.md +121 -0
  35. package/articles/PAIN-DRIVEN-twitter.md +120 -0
  36. package/articles/PORTKEY_VS_A3M.md +147 -0
  37. package/articles/REDDIT_FINAL.md +232 -0
  38. package/articles/TWITTER_FINAL.md +167 -0
  39. package/articles/WHY_10X_BETTER.md +261 -0
  40. package/articles/WHY_CHINESE_STYLE_BETTER.md +323 -0
  41. package/articles/hashnode-llm-cost-optimization.md +125 -0
  42. package/articles/medium-building-llm-router.md +205 -0
  43. package/articles/twitter-thread-cost-savings.md +98 -0
  44. package/articles/youtube-tutorial-script.md +262 -0
  45. package/assets/banner.svg +109 -0
  46. package/assets/logo.svg +68 -0
  47. package/assets/social-preview.svg +64 -0
  48. package/demo/demo-script.md +53 -0
  49. package/dist/analytics/costAnalytics.d.ts +77 -0
  50. package/dist/analytics/costAnalytics.d.ts.map +1 -0
  51. package/dist/analytics/costAnalytics.js +219 -0
  52. package/dist/analytics/costAnalytics.js.map +1 -0
  53. package/dist/cache/semanticCache.d.ts +62 -0
  54. package/dist/cache/semanticCache.d.ts.map +1 -0
  55. package/dist/cache/semanticCache.js +176 -0
  56. package/dist/cache/semanticCache.js.map +1 -0
  57. package/dist/cli.js +35 -0
  58. package/dist/index.d.ts +4 -723
  59. package/dist/index.js +11 -362
  60. package/dist/index.js.map +1 -1
  61. package/dist/integrations/langchainAdapter.d.ts +146 -0
  62. package/dist/integrations/langchainAdapter.d.ts.map +1 -0
  63. package/dist/integrations/langchainAdapter.js +731 -0
  64. package/dist/integrations/langchainAdapter.js.map +1 -0
  65. package/dist/integrations/oauth.d.ts +69 -0
  66. package/dist/integrations/oauth.d.ts.map +1 -0
  67. package/dist/integrations/oauth.js +225 -21
  68. package/dist/integrations/oauth.js.map +1 -0
  69. package/dist/memory/autoFetch.d.ts +39 -0
  70. package/dist/memory/autoFetch.d.ts.map +1 -0
  71. package/dist/memory/autoFetch.js +80 -88
  72. package/dist/memory/autoFetch.js.map +1 -0
  73. package/dist/memory/memoryTree.d.ts +76 -0
  74. package/dist/memory/memoryTree.d.ts.map +1 -0
  75. package/dist/memory/memoryTree.js +185 -130
  76. package/dist/memory/memoryTree.js.map +1 -0
  77. package/dist/memory/obsidianVault.d.ts +71 -0
  78. package/dist/memory/obsidianVault.d.ts.map +1 -0
  79. package/dist/memory/obsidianVault.js +207 -22
  80. package/dist/memory/obsidianVault.js.map +1 -0
  81. package/dist/providers/providerConfig.d.ts +49 -0
  82. package/dist/providers/providerConfig.d.ts.map +1 -0
  83. package/dist/providers/providerConfig.js +806 -401
  84. package/dist/providers/providerConfig.js.map +1 -0
  85. package/dist/security/guardrails.d.ts +76 -0
  86. package/dist/security/guardrails.d.ts.map +1 -0
  87. package/dist/security/guardrails.js +479 -0
  88. package/dist/security/guardrails.js.map +1 -0
  89. package/dist/server/dashboard.d.ts +58 -0
  90. package/dist/server/dashboard.d.ts.map +1 -0
  91. package/dist/server/dashboard.js +553 -0
  92. package/dist/server/dashboard.js.map +1 -0
  93. package/dist/server/modelMapper.d.ts +43 -0
  94. package/dist/server/modelMapper.d.ts.map +1 -0
  95. package/dist/server/modelMapper.js +154 -0
  96. package/dist/server/modelMapper.js.map +1 -0
  97. package/dist/server/proxyServer.d.ts +41 -0
  98. package/dist/server/proxyServer.d.ts.map +1 -0
  99. package/dist/server/proxyServer.js +932 -0
  100. package/dist/server/proxyServer.js.map +1 -0
  101. package/dist/skills/__tests__/skill_manager.test.d.ts +2 -0
  102. package/dist/skills/__tests__/skill_manager.test.d.ts.map +1 -0
  103. package/dist/skills/__tests__/skill_manager.test.js +268 -0
  104. package/dist/skills/__tests__/skill_manager.test.js.map +1 -0
  105. package/docs-site/index.html +347 -0
  106. package/package.json +53 -7
  107. package/playground/README.md +51 -0
  108. package/playground/codesandbox.json +12 -0
  109. package/playground/index.js +39 -0
  110. package/scripts/update-npm-badges.js +158 -0
  111. package/src/analytics/costAnalytics.ts +304 -0
  112. package/src/cache/semanticCache.ts +221 -0
  113. package/src/index.ts +6 -0
  114. package/src/integrations/langchainAdapter.ts +955 -0
  115. package/src/providers/providerConfig.ts +923 -0
  116. package/src/security/guardrails.ts +585 -0
  117. package/src/server/dashboard.ts +610 -0
  118. package/src/server/modelMapper.ts +182 -0
  119. package/src/server/proxyServer.ts +1105 -0
  120. package/src/types/langchain.d.ts +83 -0
  121. package/tsconfig.build.json +20 -0
@@ -0,0 +1,610 @@
1
+ /**
2
+ * A3M Router - Dashboard UI + API Endpoints
3
+ *
4
+ * Single HTML dashboard served at GET / when the proxy server runs.
5
+ * Dark theme, hacker/terminal aesthetic, auto-refreshes every 5s.
6
+ * API endpoints: /api/stats, /api/providers, /api/requests, /api/clear
7
+ *
8
+ * No React, no build step — vanilla HTML/CSS/JS served inline.
9
+ */
10
+
11
+ import * as http from "http";
12
+
13
+ // ============================================================
14
+ // Types
15
+ // ============================================================
16
+
17
+ export interface DashboardRequest {
18
+ timestamp: number;
19
+ query: string;
20
+ provider: string;
21
+ model: string;
22
+ latency: number;
23
+ cost: number;
24
+ status: "success" | "error";
25
+ error?: string;
26
+ tokens?: { input: number; output: number };
27
+ }
28
+
29
+ export interface DashboardProvider {
30
+ id: string;
31
+ name: string;
32
+ status: "online" | "offline";
33
+ requestsToday: number;
34
+ costToday: number;
35
+ avgLatency: number;
36
+ lastError: string | null;
37
+ lastErrorTime: number | null;
38
+ }
39
+
40
+ export interface DashboardStats {
41
+ totalRequestsToday: number;
42
+ totalCostToday: number;
43
+ avgLatency: number;
44
+ activeProviders: number;
45
+ totalProviders: number;
46
+ providers: DashboardProvider[];
47
+ recentRequests: DashboardRequest[];
48
+ costByProvider: Record<string, number>;
49
+ uptime: number;
50
+ }
51
+
52
+ // ============================================================
53
+ // Dashboard State (in-memory)
54
+ // ============================================================
55
+
56
+ const MAX_REQUESTS = 50;
57
+ const recentRequests: DashboardRequest[] = [];
58
+ const providerStats: Map<string, DashboardProvider> = new Map();
59
+ const startTime = Date.now();
60
+
61
+ /**
62
+ * Record a request for the dashboard.
63
+ */
64
+ export function recordRequest(req: Omit<DashboardRequest, "timestamp">): void {
65
+ const entry: DashboardRequest = { ...req, timestamp: Date.now() };
66
+ recentRequests.unshift(entry);
67
+ if (recentRequests.length > MAX_REQUESTS) recentRequests.pop();
68
+
69
+ // Update provider stats
70
+ const existing = providerStats.get(req.provider);
71
+ const today = startOfToday();
72
+ const isToday = true; // We track rolling
73
+
74
+ if (existing) {
75
+ existing.requestsToday++;
76
+ existing.costToday += req.cost;
77
+ // Running average latency
78
+ existing.avgLatency =
79
+ (existing.avgLatency * (existing.requestsToday - 1) + req.latency) /
80
+ existing.requestsToday;
81
+ if (req.status === "error") {
82
+ existing.lastError = req.error || "Unknown error";
83
+ existing.lastErrorTime = entry.timestamp;
84
+ existing.status = "offline";
85
+ } else {
86
+ existing.status = "online";
87
+ }
88
+ } else {
89
+ providerStats.set(req.provider, {
90
+ id: req.provider,
91
+ name: req.provider,
92
+ status: req.status === "success" ? "online" : "offline",
93
+ requestsToday: 1,
94
+ costToday: req.cost,
95
+ avgLatency: req.latency,
96
+ lastError: req.status === "error" ? req.error || "Unknown error" : null,
97
+ lastErrorTime: req.status === "error" ? entry.timestamp : null,
98
+ });
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Register a provider as available (called on startup / health check).
104
+ */
105
+ export function registerProvider(id: string, name: string): void {
106
+ if (!providerStats.has(id)) {
107
+ providerStats.set(id, {
108
+ id,
109
+ name,
110
+ status: "online",
111
+ requestsToday: 0,
112
+ costToday: 0,
113
+ avgLatency: 0,
114
+ lastError: null,
115
+ lastErrorTime: null,
116
+ });
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Build the full stats snapshot.
122
+ */
123
+ function buildStats(): DashboardStats {
124
+ const providers = Array.from(providerStats.values());
125
+ const active = providers.filter((p) => p.status === "online").length;
126
+ const totalCost = providers.reduce((s, p) => s + p.costToday, 0);
127
+ const totalReqs = providers.reduce((s, p) => s + p.requestsToday, 0);
128
+ const latReqs = providers.filter((p) => p.avgLatency > 0);
129
+ const avgLat =
130
+ latReqs.length > 0
131
+ ? latReqs.reduce((s, p) => s + p.avgLatency, 0) / latReqs.length
132
+ : 0;
133
+
134
+ const costByProvider: Record<string, number> = {};
135
+ for (const p of providers) {
136
+ costByProvider[p.id] = p.costToday;
137
+ }
138
+
139
+ return {
140
+ totalRequestsToday: totalReqs,
141
+ totalCostToday: totalCost,
142
+ avgLatency: avgLat,
143
+ activeProviders: active,
144
+ totalProviders: providers.length,
145
+ providers,
146
+ recentRequests,
147
+ costByProvider,
148
+ uptime: Date.now() - startTime,
149
+ };
150
+ }
151
+
152
+ function startOfToday(): number {
153
+ const d = new Date();
154
+ d.setHours(0, 0, 0, 0);
155
+ return d.getTime();
156
+ }
157
+
158
+ // ============================================================
159
+ // API Route Handler
160
+ // ============================================================
161
+
162
+ /**
163
+ * Handle dashboard API requests. Returns true if the request was handled.
164
+ */
165
+ export function handleDashboardRequest(
166
+ req: http.IncomingMessage,
167
+ res: http.ServerResponse
168
+ ): boolean {
169
+ const url = req.url || "/";
170
+
171
+ // CORS headers
172
+ res.setHeader("Access-Control-Allow-Origin", "*");
173
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
174
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
175
+
176
+ if (req.method === "OPTIONS") {
177
+ res.writeHead(204);
178
+ res.end();
179
+ return true;
180
+ }
181
+
182
+ // GET / → Dashboard HTML
183
+ if (url === "/" && req.method === "GET") {
184
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
185
+ res.end(getDashboardHTML());
186
+ return true;
187
+ }
188
+
189
+ // GET /api/stats
190
+ if (url === "/api/stats" && req.method === "GET") {
191
+ res.writeHead(200, { "Content-Type": "application/json" });
192
+ res.end(JSON.stringify(buildStats()));
193
+ return true;
194
+ }
195
+
196
+ // GET /api/providers
197
+ if (url === "/api/providers" && req.method === "GET") {
198
+ res.writeHead(200, { "Content-Type": "application/json" });
199
+ res.end(JSON.stringify(Array.from(providerStats.values())));
200
+ return true;
201
+ }
202
+
203
+ // GET /api/requests
204
+ if (url === "/api/requests" && req.method === "GET") {
205
+ res.writeHead(200, { "Content-Type": "application/json" });
206
+ res.end(JSON.stringify(recentRequests));
207
+ return true;
208
+ }
209
+
210
+ // POST /api/clear
211
+ if (url === "/api/clear" && req.method === "POST") {
212
+ recentRequests.length = 0;
213
+ for (const p of providerStats.values()) {
214
+ p.requestsToday = 0;
215
+ p.costToday = 0;
216
+ p.avgLatency = 0;
217
+ p.lastError = null;
218
+ p.lastErrorTime = null;
219
+ }
220
+ res.writeHead(200, { "Content-Type": "application/json" });
221
+ res.end(JSON.stringify({ ok: true, message: "Dashboard cleared" }));
222
+ return true;
223
+ }
224
+
225
+ return false; // Not a dashboard route
226
+ }
227
+
228
+ // ============================================================
229
+ // Dashboard HTML
230
+ // ============================================================
231
+
232
+ export function getDashboardHTML(): string {
233
+ return `<!DOCTYPE html>
234
+ <html lang="en">
235
+ <head>
236
+ <meta charset="UTF-8">
237
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
238
+ <title>A3M Router Dashboard</title>
239
+ <style>
240
+ * { margin:0; padding:0; box-sizing:border-box; }
241
+ :root {
242
+ --bg: #0a0a0f;
243
+ --surface: #111118;
244
+ --surface2: #1a1a24;
245
+ --border: #2a2a3a;
246
+ --text: #c8c8d8;
247
+ --text-dim: #6a6a80;
248
+ --green: #00ff88;
249
+ --green-dim: #00aa5a;
250
+ --red: #ff4444;
251
+ --yellow: #ffcc00;
252
+ --cyan: #00ccff;
253
+ --magenta: #ff44ff;
254
+ --orange: #ff8800;
255
+ --mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Consolas', monospace;
256
+ }
257
+ body {
258
+ background: var(--bg);
259
+ color: var(--text);
260
+ font-family: var(--mono);
261
+ font-size: 13px;
262
+ line-height: 1.5;
263
+ padding: 20px;
264
+ min-height: 100vh;
265
+ }
266
+ a { color: var(--cyan); text-decoration: none; }
267
+
268
+ /* Header */
269
+ .header {
270
+ border-bottom: 1px solid var(--green-dim);
271
+ padding-bottom: 12px;
272
+ margin-bottom: 24px;
273
+ display: flex;
274
+ justify-content: space-between;
275
+ align-items: center;
276
+ }
277
+ .header h1 {
278
+ color: var(--green);
279
+ font-size: 18px;
280
+ font-weight: 600;
281
+ letter-spacing: 2px;
282
+ }
283
+ .header .uptime { color: var(--text-dim); font-size: 11px; }
284
+
285
+ /* Overview cards */
286
+ .cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 28px; }
287
+ .card {
288
+ background: var(--surface);
289
+ border: 1px solid var(--border);
290
+ border-radius: 6px;
291
+ padding: 16px;
292
+ position: relative;
293
+ overflow: hidden;
294
+ }
295
+ .card::before {
296
+ content: '';
297
+ position: absolute;
298
+ top: 0; left: 0; right: 0;
299
+ height: 2px;
300
+ background: var(--green);
301
+ opacity: 0.6;
302
+ }
303
+ .card.error::before { background: var(--red); }
304
+ .card .label { color: var(--text-dim); font-size: 10px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
305
+ .card .value { font-size: 22px; font-weight: 700; color: var(--green); }
306
+ .card .value.cost { color: var(--yellow); }
307
+ .card .value.latency { color: var(--cyan); }
308
+ .card .value.providers { color: var(--magenta); }
309
+
310
+ /* Section */
311
+ .section { margin-bottom: 28px; }
312
+ .section-title {
313
+ color: var(--green);
314
+ font-size: 13px;
315
+ font-weight: 600;
316
+ text-transform: uppercase;
317
+ letter-spacing: 1.5px;
318
+ margin-bottom: 12px;
319
+ padding-left: 10px;
320
+ border-left: 3px solid var(--green);
321
+ }
322
+
323
+ /* Provider table */
324
+ table { width: 100%; border-collapse: collapse; }
325
+ th {
326
+ text-align: left;
327
+ color: var(--text-dim);
328
+ font-size: 10px;
329
+ text-transform: uppercase;
330
+ letter-spacing: 1px;
331
+ padding: 8px 12px;
332
+ border-bottom: 1px solid var(--border);
333
+ }
334
+ td {
335
+ padding: 8px 12px;
336
+ border-bottom: 1px solid var(--border);
337
+ font-size: 12px;
338
+ }
339
+ tr:hover { background: var(--surface2); }
340
+ .status-dot { font-size: 14px; }
341
+ .error-text { color: var(--red); font-size: 11px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
342
+
343
+ /* Cost chart */
344
+ .chart-row { display: flex; align-items: center; margin-bottom: 8px; gap: 12px; }
345
+ .chart-label { width: 100px; text-align: right; color: var(--text-dim); font-size: 11px; flex-shrink:0; }
346
+ .chart-bar-wrap { flex: 1; background: var(--surface); border-radius: 3px; height: 20px; position: relative; }
347
+ .chart-bar {
348
+ height: 100%;
349
+ border-radius: 3px;
350
+ min-width: 2px;
351
+ transition: width 0.5s ease;
352
+ position: relative;
353
+ }
354
+ .chart-bar .chart-val {
355
+ position: absolute;
356
+ right: 6px;
357
+ top: 50%;
358
+ transform: translateY(-50%);
359
+ font-size: 10px;
360
+ color: var(--bg);
361
+ font-weight: 600;
362
+ }
363
+
364
+ /* Request log */
365
+ .log-wrap { max-height: 420px; overflow-y: auto; }
366
+ .log-wrap::-webkit-scrollbar { width: 6px; }
367
+ .log-wrap::-webkit-scrollbar-track { background: var(--surface); }
368
+ .log-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
369
+ .query-cell { color: var(--text); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
370
+ .badge {
371
+ display: inline-block;
372
+ padding: 1px 6px;
373
+ border-radius: 3px;
374
+ font-size: 10px;
375
+ font-weight: 600;
376
+ }
377
+ .badge.success { background: #0a2a1a; color: var(--green); }
378
+ .badge.error { background: #2a0a0a; color: var(--red); }
379
+
380
+ /* Footer */
381
+ .footer {
382
+ margin-top: 32px;
383
+ padding-top: 12px;
384
+ border-top: 1px solid var(--border);
385
+ color: var(--text-dim);
386
+ font-size: 10px;
387
+ text-align: center;
388
+ }
389
+ .refresh-indicator { color: var(--green-dim); }
390
+
391
+ /* Responsive */
392
+ @media (max-width: 900px) {
393
+ .cards { grid-template-columns: repeat(2, 1fr); }
394
+ }
395
+ @media (max-width: 500px) {
396
+ .cards { grid-template-columns: 1fr; }
397
+ }
398
+ </style>
399
+ </head>
400
+ <body>
401
+
402
+ <div class="header">
403
+ <h1>&gt;_ A3M ROUTER</h1>
404
+ <div>
405
+ <span class="uptime" id="uptime">uptime: loading...</span>
406
+ <span class="refresh-indicator" id="refresh-dot"> &#9679;</span>
407
+ </div>
408
+ </div>
409
+
410
+ <!-- Overview Cards -->
411
+ <div class="cards" id="overview-cards">
412
+ <div class="card">
413
+ <div class="label">Requests Today</div>
414
+ <div class="value" id="stat-requests">--</div>
415
+ </div>
416
+ <div class="card">
417
+ <div class="label">Cost Today</div>
418
+ <div class="value cost" id="stat-cost">$--</div>
419
+ </div>
420
+ <div class="card">
421
+ <div class="label">Avg Latency</div>
422
+ <div class="value latency" id="stat-latency">-- ms</div>
423
+ </div>
424
+ <div class="card">
425
+ <div class="label">Active Providers</div>
426
+ <div class="value providers" id="stat-providers">-- / --</div>
427
+ </div>
428
+ </div>
429
+
430
+ <!-- Provider Status -->
431
+ <div class="section">
432
+ <div class="section-title">Provider Status</div>
433
+ <table>
434
+ <thead>
435
+ <tr>
436
+ <th>Status</th>
437
+ <th>Provider</th>
438
+ <th>Requests</th>
439
+ <th>Cost</th>
440
+ <th>Avg Latency</th>
441
+ <th>Last Error</th>
442
+ </tr>
443
+ </thead>
444
+ <tbody id="provider-table"></tbody>
445
+ </table>
446
+ </div>
447
+
448
+ <!-- Cost Chart -->
449
+ <div class="section">
450
+ <div class="section-title">Cost by Provider</div>
451
+ <div id="cost-chart">
452
+ <div style="color:var(--text-dim); font-size:11px;">Loading...</div>
453
+ </div>
454
+ </div>
455
+
456
+ <!-- Request Log -->
457
+ <div class="section">
458
+ <div class="section-title">Recent Requests <span style="color:var(--text-dim);font-weight:400;">(last 50)</span></div>
459
+ <div class="log-wrap">
460
+ <table>
461
+ <thead>
462
+ <tr>
463
+ <th>Time</th>
464
+ <th>Query</th>
465
+ <th>Provider</th>
466
+ <th>Model</th>
467
+ <th>Latency</th>
468
+ <th>Cost</th>
469
+ <th>Status</th>
470
+ </tr>
471
+ </thead>
472
+ <tbody id="request-log"></tbody>
473
+ </table>
474
+ </div>
475
+ </div>
476
+
477
+ <!-- Clear + Footer -->
478
+ <div style="text-align:right; margin-bottom:12px;">
479
+ <button onclick="clearLog()" style="
480
+ background:var(--surface2); color:var(--red); border:1px solid var(--red);
481
+ padding:6px 16px; border-radius:4px; cursor:pointer; font-family:var(--mono);
482
+ font-size:11px; letter-spacing:1px;
483
+ ">CLEAR LOG</button>
484
+ </div>
485
+
486
+ <div class="footer">
487
+ A3M Router Dashboard &middot; Auto-refresh every 5s &middot; <span id="last-refresh">--</span>
488
+ </div>
489
+
490
+ <script>
491
+ const CHART_COLORS = ['#00ff88','#00ccff','#ffcc00','#ff44ff','#ff8800','#ff4444','#88ff00','#44ffff'];
492
+
493
+ function fmt(n, d) { return typeof n === 'number' ? n.toFixed(d || 2) : '--'; }
494
+ function fmtCost(c) { return '$' + fmt(c, 6); }
495
+ function fmtMs(ms) { return fmt(ms, 0) + ' ms'; }
496
+ function fmtTime(ts) {
497
+ const d = new Date(ts);
498
+ return d.toLocaleTimeString('en-US', { hour12: false, hour:'2-digit', minute:'2-digit', second:'2-digit' });
499
+ }
500
+ function uptime(ms) {
501
+ const s = Math.floor(ms/1000);
502
+ const h = Math.floor(s/3600);
503
+ const m = Math.floor((s%3600)/60);
504
+ return 'uptime: ' + h + 'h ' + m + 'm';
505
+ }
506
+ function esc(s) {
507
+ if (!s) return '';
508
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
509
+ }
510
+ function trunc(s, n) { return s && s.length > n ? s.substring(0, n) + '...' : s; }
511
+
512
+ function renderProviders(providers) {
513
+ const tbody = document.getElementById('provider-table');
514
+ if (!providers || providers.length === 0) {
515
+ tbody.innerHTML = '<tr><td colspan="6" style="color:var(--text-dim);">No providers registered</td></tr>';
516
+ return;
517
+ }
518
+ tbody.innerHTML = providers.map(function(p) {
519
+ var dot = p.status === 'online' ? '&#x1F7E2;' : '&#x1F534;';
520
+ var errCell = p.lastError
521
+ ? '<td class="error-text" title="' + esc(p.lastError) + '">' + esc(trunc(p.lastError, 30)) + '</td>'
522
+ : '<td style="color:var(--text-dim);">--</td>';
523
+ return '<tr>' +
524
+ '<td class="status-dot">' + dot + '</td>' +
525
+ '<td>' + esc(p.name || p.id) + '</td>' +
526
+ '<td>' + p.requestsToday + '</td>' +
527
+ '<td>' + fmtCost(p.costToday) + '</td>' +
528
+ '<td>' + (p.avgLatency > 0 ? fmtMs(p.avgLatency) : '--') + '</td>' +
529
+ errCell +
530
+ '</tr>';
531
+ }).join('');
532
+ }
533
+
534
+ function renderChart(costByProvider) {
535
+ var el = document.getElementById('cost-chart');
536
+ var keys = Object.keys(costByProvider || {});
537
+ if (keys.length === 0) {
538
+ el.innerHTML = '<div style="color:var(--text-dim);font-size:11px;">No cost data yet</div>';
539
+ return;
540
+ }
541
+ var max = 0;
542
+ keys.forEach(function(k) { if (costByProvider[k] > max) max = costByProvider[k]; });
543
+ if (max === 0) max = 0.001;
544
+ el.innerHTML = keys.map(function(k, i) {
545
+ var pct = Math.max(1, (costByProvider[k] / max) * 100);
546
+ var color = CHART_COLORS[i % CHART_COLORS.length];
547
+ return '<div class="chart-row">' +
548
+ '<div class="chart-label">' + esc(k) + '</div>' +
549
+ '<div class="chart-bar-wrap">' +
550
+ '<div class="chart-bar" style="width:' + pct + '%;background:' + color + ';">' +
551
+ '<span class="chart-val">' + fmtCost(costByProvider[k]) + '</span>' +
552
+ '</div>' +
553
+ '</div>' +
554
+ '</div>';
555
+ }).join('');
556
+ }
557
+
558
+ function renderRequests(requests) {
559
+ var tbody = document.getElementById('request-log');
560
+ if (!requests || requests.length === 0) {
561
+ tbody.innerHTML = '<tr><td colspan="7" style="color:var(--text-dim);">No requests yet</td></tr>';
562
+ return;
563
+ }
564
+ tbody.innerHTML = requests.map(function(r) {
565
+ var badge = r.status === 'success'
566
+ ? '<span class="badge success">OK</span>'
567
+ : '<span class="badge error">ERR</span>';
568
+ return '<tr>' +
569
+ '<td>' + fmtTime(r.timestamp) + '</td>' +
570
+ '<td class="query-cell" title="' + esc(r.query) + '">' + esc(trunc(r.query || '', 50)) + '</td>' +
571
+ '<td>' + esc(r.provider) + '</td>' +
572
+ '<td style="font-size:10px;color:var(--text-dim);">' + esc(r.model) + '</td>' +
573
+ '<td>' + fmtMs(r.latency) + '</td>' +
574
+ '<td>' + fmtCost(r.cost) + '</td>' +
575
+ '<td>' + badge + '</td>' +
576
+ '</tr>';
577
+ }).join('');
578
+ }
579
+
580
+ function clearLog() {
581
+ fetch('/api/clear', { method: 'POST' }).then(function() { refresh(); });
582
+ }
583
+
584
+ async function refresh() {
585
+ try {
586
+ var resp = await fetch('/api/stats');
587
+ var data = await resp.json();
588
+ document.getElementById('stat-requests').textContent = data.totalRequestsToday || 0;
589
+ document.getElementById('stat-cost').textContent = fmtCost(data.totalCostToday);
590
+ document.getElementById('stat-latency').textContent = data.avgLatency > 0 ? fmtMs(data.avgLatency) : '-- ms';
591
+ document.getElementById('stat-providers').textContent = (data.activeProviders || 0) + ' / ' + (data.totalProviders || 0);
592
+ document.getElementById('uptime').textContent = uptime(data.uptime || 0);
593
+ renderProviders(data.providers);
594
+ renderChart(data.costByProvider);
595
+ renderRequests(data.recentRequests);
596
+ document.getElementById('last-refresh').textContent = 'updated ' + new Date().toLocaleTimeString();
597
+ document.getElementById('refresh-dot').style.color = '#00ff88';
598
+ setTimeout(function() { document.getElementById('refresh-dot').style.color = '#00aa5a'; }, 800);
599
+ } catch (e) {
600
+ document.getElementById('last-refresh').textContent = 'fetch error: ' + e.message;
601
+ document.getElementById('refresh-dot').style.color = '#ff4444';
602
+ }
603
+ }
604
+
605
+ refresh();
606
+ setInterval(refresh, 5000);
607
+ </script>
608
+ </body>
609
+ </html>`;
610
+ }