adaptive-memory-multi-model-router 2.12.7 → 2.13.1
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/README.md +256 -753
- package/dist/tui/dashboard.d.ts +5 -12
- package/dist/tui/dashboard.js +470 -270
- package/dist/tui/dashboard.js.map +1 -1
- package/package.json +2 -2
- package/src/tui/dashboard.ts +502 -313
- package/tmlpd-pi-extension/README.md +52 -22
- package/tmlpd-pi-extension/package-lock.json +5 -1
- package/tmlpd-pi-extension/package.json +2 -2
- package/tmlpd-pi-extension/src/index.ts +8 -0
- package/tmlpd-pi-extension/src/memory/episodicMemory.ts +74 -2
- package/tmlpd-pi-extension/src/routing/ensembleVoting.ts +159 -0
- package/tmlpd-pi-extension/src/routing/queryTypePresets.ts +136 -0
package/src/tui/dashboard.ts
CHANGED
|
@@ -1,407 +1,596 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* A3M Router TUI —
|
|
3
|
+
* A3M Router TUI v2 — 10x UI
|
|
4
4
|
*
|
|
5
|
-
* Inspired by: k9s
|
|
5
|
+
* Inspired by: lazygit panels + k9s pulse + btop graphs + Tokyo Night theme
|
|
6
6
|
* Built with: blessed + blessed-contrib
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* c — Cost breakdown view
|
|
12
|
-
* p — Provider detail view
|
|
13
|
-
* l — Live request log
|
|
14
|
-
* b — Budget alerts
|
|
15
|
-
* q — Quit
|
|
16
|
-
* tab — Switch panel focus
|
|
17
|
-
* ↑↓ — Navigate lists
|
|
8
|
+
* TABS: 1-Dashboard 2-Costs 3-Providers 4-Logs 5-Help
|
|
9
|
+
* F-KEYS: F1 Dash F2 Costs F3 Prov F4 Logs F5 Help F10 Quit
|
|
10
|
+
* vim/hjkl for list nav, mouse for click targets
|
|
18
11
|
*/
|
|
19
12
|
|
|
20
13
|
import * as blessed from 'blessed';
|
|
21
14
|
import * as contrib from 'blessed-contrib';
|
|
22
|
-
import * as fs from 'fs';
|
|
23
|
-
import * as path from 'path';
|
|
24
|
-
|
|
25
|
-
// ============================================================
|
|
26
|
-
// Mock data (in production, fetches from live A3M proxy)
|
|
27
|
-
// ============================================================
|
|
28
|
-
|
|
29
|
-
interface ProviderStatus {
|
|
30
|
-
name: string;
|
|
31
|
-
model: string;
|
|
32
|
-
healthy: boolean;
|
|
33
|
-
latency: number;
|
|
34
|
-
costPerK: number;
|
|
35
|
-
requests: number;
|
|
36
|
-
tier: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface CostSnapshot {
|
|
40
|
-
total: number;
|
|
41
|
-
daily: Record<string, number>;
|
|
42
|
-
monthly: Record<string, number>;
|
|
43
|
-
byProvider: Record<string, number>;
|
|
44
|
-
requestCount: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface RequestLog {
|
|
48
|
-
timestamp: string;
|
|
49
|
-
model: string;
|
|
50
|
-
provider: string;
|
|
51
|
-
latency: number;
|
|
52
|
-
tokens: number;
|
|
53
|
-
cost: number;
|
|
54
|
-
status: number;
|
|
55
|
-
}
|
|
56
15
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════
|
|
17
|
+
// TOKYO NIGHT THEME
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
const T = {
|
|
21
|
+
bg: '#1a1b26',
|
|
22
|
+
bgDark: '#16161e',
|
|
23
|
+
surface: '#24283b',
|
|
24
|
+
border: '#3b4261',
|
|
25
|
+
accent: '#7aa2f7', // blue
|
|
26
|
+
accent2: '#bb9af7', // purple
|
|
27
|
+
green: '#9ece6a',
|
|
28
|
+
yellow: '#e0af68',
|
|
29
|
+
red: '#f7768e',
|
|
30
|
+
cyan: '#7dcfff',
|
|
31
|
+
magenta: '#bb9af7',
|
|
32
|
+
orange: '#ff9e64',
|
|
33
|
+
white: '#c0caf5',
|
|
34
|
+
dim: '#565f89',
|
|
35
|
+
bright: '#a9b1d6',
|
|
36
|
+
pink: '#ff007c',
|
|
75
37
|
};
|
|
76
38
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{ timestamp: '10:30:11', model: 'llama-3.1-8b', provider: 'nvidia', latency: 92, tokens: 52, cost: 0, status: 200 },
|
|
81
|
-
{ timestamp: '10:29:03', model: 'llama3', provider: 'ollama', latency: 55, tokens: 31, cost: 0, status: 200 },
|
|
82
|
-
];
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════
|
|
40
|
+
// STATE
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════
|
|
83
42
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
const C = {
|
|
89
|
-
bg: '#0a0e14',
|
|
90
|
-
surface: '#131820',
|
|
91
|
-
border: '#253040',
|
|
92
|
-
accent: '#39bae6',
|
|
93
|
-
accent2: '#ff8f40',
|
|
94
|
-
green: '#7fd962',
|
|
95
|
-
yellow: '#ffcc66',
|
|
96
|
-
red: '#f26d78',
|
|
97
|
-
white: '#bfbab0',
|
|
98
|
-
dim: '#5c6773',
|
|
99
|
-
bright: '#e6e1cf',
|
|
100
|
-
header: '#d4bfff',
|
|
101
|
-
};
|
|
43
|
+
let activeTab = 1;
|
|
44
|
+
let tick = 0;
|
|
45
|
+
const MAX_HISTORY = 60; // 60 data points for sparklines
|
|
102
46
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
premium: C.accent2,
|
|
110
|
-
local: C.dim,
|
|
111
|
-
};
|
|
47
|
+
// Cost history (simulated time series)
|
|
48
|
+
const costHistory: number[] = Array(MAX_HISTORY).fill(0);
|
|
49
|
+
const latencyHistory: number[] = Array(MAX_HISTORY).fill(30);
|
|
50
|
+
|
|
51
|
+
// Provider history for live charts
|
|
52
|
+
const providerLoads: Record<string, number[]> = {};
|
|
112
53
|
|
|
113
|
-
//
|
|
114
|
-
// SCREEN
|
|
115
|
-
//
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════
|
|
55
|
+
// SCREEN
|
|
56
|
+
// ═══════════════════════════════════════════════════════════════
|
|
116
57
|
|
|
117
58
|
const screen = blessed.screen({
|
|
118
59
|
smartCSR: true,
|
|
119
|
-
title: 'A3M Router',
|
|
120
|
-
dockBorders: false,
|
|
60
|
+
title: 'A3M Router ⚡',
|
|
121
61
|
fullUnicode: true,
|
|
122
|
-
|
|
62
|
+
mouse: true,
|
|
63
|
+
// @ts-ignore
|
|
64
|
+
cursor: { shape: 'line', blink: true },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const GRID = new contrib.grid({ rows: 12, cols: 24, screen });
|
|
68
|
+
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════
|
|
70
|
+
// TITLE BAR — Tokyo Night Header
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════
|
|
72
|
+
|
|
73
|
+
const titleBar = GRID.set(0, 0, 1, 24, blessed.box, {
|
|
74
|
+
style: { fg: T.bright, bg: T.bgDark },
|
|
75
|
+
tags: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function renderTitleBar() {
|
|
79
|
+
titleBar.setContent(
|
|
80
|
+
` {bold}{#bb9af7-fg}⚡ A3M Router{/} {#565f89-fg}v2.12.7{/} │ ` +
|
|
81
|
+
`[F1] {${activeTab===1?'#7aa2f7':'#565f89'}-fg}Dashboard{/}` +
|
|
82
|
+
` [F2] {${activeTab===2?'#7aa2f7':'#565f89'}-fg}Costs{/}` +
|
|
83
|
+
` [F3] {${activeTab===3?'#7aa2f7':'#565f89'}-fg}Providers{/}` +
|
|
84
|
+
` [F4] {${activeTab===4?'#7aa2f7':'#565f89'}-fg}Logs{/}` +
|
|
85
|
+
` [F5] {${activeTab===5?'#7aa2f7':'#565f89'}-fg}Help{/}` +
|
|
86
|
+
` │ {#565f89-fg}q quit / cmd ↑↓ nav tab switch{/}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ═══════════════════════════════════════════════════════════════
|
|
91
|
+
// TAB 1: DASHBOARD — Sparklines + Provider Grid + Live Feed
|
|
92
|
+
// ═══════════════════════════════════════════════════════════════
|
|
93
|
+
|
|
94
|
+
// --- Latency Sparkline ---
|
|
95
|
+
const latencySpark = GRID.set(1, 0, 3, 8, contrib.sparkline, {
|
|
96
|
+
label: ' ▸ Latency History (ms)',
|
|
97
|
+
style: { line: T.accent, text: T.white, baseline: T.dim },
|
|
98
|
+
tags: true,
|
|
123
99
|
});
|
|
124
100
|
|
|
125
|
-
|
|
101
|
+
// --- Cost Sparkline ---
|
|
102
|
+
const costSpark = GRID.set(1, 8, 3, 8, contrib.sparkline, {
|
|
103
|
+
label: ' ▸ Cost Rate ($/1K req)',
|
|
104
|
+
style: { line: T.green, text: T.white, baseline: T.dim },
|
|
105
|
+
tags: true,
|
|
106
|
+
});
|
|
126
107
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
108
|
+
// --- Provider Health Bars ---
|
|
109
|
+
const providerBars = GRID.set(1, 16, 3, 8, blessed.box, {
|
|
110
|
+
label: ' ▸ Provider Health',
|
|
111
|
+
border: { type: 'line', fg: T.border },
|
|
112
|
+
style: { fg: T.white, bg: T.bg },
|
|
113
|
+
tags: true,
|
|
114
|
+
});
|
|
130
115
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
116
|
+
// --- Live Request Feed (scrolling) ---
|
|
117
|
+
const liveFeed = GRID.set(4, 0, 4, 14, contrib.log, {
|
|
118
|
+
fg: T.white,
|
|
119
|
+
selectedFg: T.green,
|
|
120
|
+
label: ' ▸ Live Request Feed',
|
|
121
|
+
border: { type: 'line', fg: T.border },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// --- Routing Map ---
|
|
125
|
+
const routeMap = GRID.set(4, 14, 4, 10, blessed.box, {
|
|
126
|
+
label: ' ▸ Routing Intelligence',
|
|
127
|
+
border: { type: 'line', fg: T.border },
|
|
128
|
+
style: { fg: T.white, bg: T.bg },
|
|
129
|
+
tags: true,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// --- KPI Boxes ---
|
|
133
|
+
const kpiRow = GRID.set(8, 0, 2, 24, blessed.box, {
|
|
134
|
+
style: { fg: T.white, bg: T.bgDark },
|
|
134
135
|
tags: true,
|
|
135
136
|
});
|
|
136
137
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
138
|
+
// ═══════════════════════════════════════════════════════════════
|
|
139
|
+
// TAB 2: COSTS — Detailed breakdown
|
|
140
|
+
// ═══════════════════════════════════════════════════════════════
|
|
141
|
+
|
|
142
|
+
const costDetail = GRID.set(1, 0, 9, 24, blessed.box, {
|
|
143
|
+
label: ' ▸ Cost Analytics',
|
|
144
|
+
border: { type: 'line', fg: T.border },
|
|
145
|
+
style: { fg: T.white, bg: T.bg },
|
|
146
|
+
tags: true,
|
|
147
|
+
});
|
|
140
148
|
|
|
141
|
-
|
|
149
|
+
// ═══════════════════════════════════════════════════════════════
|
|
150
|
+
// TAB 3: PROVIDERS — Detailed provider table
|
|
151
|
+
// ═══════════════════════════════════════════════════════════════
|
|
152
|
+
|
|
153
|
+
const providerDetail = GRID.set(1, 0, 9, 24, contrib.table, {
|
|
142
154
|
keys: true,
|
|
143
|
-
fg:
|
|
144
|
-
selectedFg:
|
|
145
|
-
selectedBg:
|
|
155
|
+
fg: T.white,
|
|
156
|
+
selectedFg: T.bright,
|
|
157
|
+
selectedBg: T.border,
|
|
146
158
|
interactive: true,
|
|
147
|
-
label: ' ▸
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
columnSpacing: 2,
|
|
152
|
-
columnWidth: [12, 14, 6, 8, 8],
|
|
159
|
+
label: ' ▸ Provider Registry',
|
|
160
|
+
border: { type: 'line', fg: T.border },
|
|
161
|
+
columnSpacing: 3,
|
|
162
|
+
columnWidth: [12, 18, 10, 10, 13, 12, 16],
|
|
153
163
|
});
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
p.model,
|
|
159
|
-
p.tier.toUpperCase(),
|
|
160
|
-
p.healthy ? `● ${p.latency}ms` : '○ OFFLINE',
|
|
161
|
-
p.requests > 0 ? `${p.requests}` : '—',
|
|
162
|
-
]);
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════
|
|
166
|
+
// TAB 4: LOGS — Full request history
|
|
167
|
+
// ═══════════════════════════════════════════════════════════════
|
|
163
168
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
const fullLog = GRID.set(1, 0, 9, 24, contrib.log, {
|
|
170
|
+
fg: T.white,
|
|
171
|
+
selectedFg: T.green,
|
|
172
|
+
label: ' ▸ Full Request Log',
|
|
173
|
+
border: { type: 'line', fg: T.border },
|
|
174
|
+
});
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
providers.forEach((p, i) => {
|
|
173
|
-
if (rows[i]) {
|
|
174
|
-
const color = p.healthy ? colorScheme[p.tier] || C.white : C.red;
|
|
175
|
-
rows[i].style.fg = color;
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
176
|
+
// ═══════════════════════════════════════════════════════════════
|
|
177
|
+
// TAB 5: HELP
|
|
178
|
+
// ═══════════════════════════════════════════════════════════════
|
|
180
179
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
const helpPanel = GRID.set(1, 0, 9, 24, blessed.box, {
|
|
181
|
+
label: ' ▸ Keyboard Reference',
|
|
182
|
+
border: { type: 'line', fg: T.border },
|
|
183
|
+
style: { fg: T.white, bg: T.bg },
|
|
184
|
+
tags: true,
|
|
185
|
+
});
|
|
184
186
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
// ═══════════════════════════════════════════════════════════════
|
|
188
|
+
// COMMAND BAR
|
|
189
|
+
// ═══════════════════════════════════════════════════════════════
|
|
190
|
+
|
|
191
|
+
const cmdBar = GRID.set(10, 0, 1, 24, blessed.textbox, {
|
|
192
|
+
label: ' ▸ /',
|
|
193
|
+
border: { type: 'line', fg: T.accent2 },
|
|
194
|
+
style: { fg: T.bright, bg: T.surface },
|
|
195
|
+
inputOnFocus: true,
|
|
196
|
+
keys: true,
|
|
197
|
+
tags: true,
|
|
189
198
|
});
|
|
190
199
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
200
|
+
// ═══════════════════════════════════════════════════════════════
|
|
201
|
+
// STATUS LINE
|
|
202
|
+
// ═══════════════════════════════════════════════════════════════
|
|
203
|
+
|
|
204
|
+
const statusLine = GRID.set(11, 0, 1, 24, blessed.box, {
|
|
205
|
+
style: { fg: T.dim, bg: T.bgDark },
|
|
195
206
|
tags: true,
|
|
196
207
|
});
|
|
197
208
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════
|
|
210
|
+
// HELPER: Render a bar chart inline
|
|
211
|
+
// ═══════════════════════════════════════════════════════════════
|
|
212
|
+
|
|
213
|
+
function barChart(value: number, max: number, width: number, chars: string[]): string {
|
|
214
|
+
const pct = Math.min(value / max, 1);
|
|
215
|
+
const filled = Math.round(pct * width);
|
|
216
|
+
const blocks = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
|
|
217
|
+
let result = '';
|
|
218
|
+
for (let i = 0; i < width; i++) {
|
|
219
|
+
if (i < filled - 1) result += '█';
|
|
220
|
+
else if (i === filled - 1) result += blocks[Math.floor(pct * width * 8) % 8] || '█';
|
|
221
|
+
else result += chars[0] || '░';
|
|
207
222
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ═══════════════════════════════════════════════════════════════
|
|
227
|
+
// HELPER: Pulse animation
|
|
228
|
+
// ═══════════════════════════════════════════════════════════════
|
|
211
229
|
|
|
212
|
-
|
|
230
|
+
function pulse(frame: number): number {
|
|
231
|
+
return Math.sin(frame * 0.3) * 0.5 + 0.5;
|
|
213
232
|
}
|
|
214
233
|
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════
|
|
235
|
+
// RENDER: Dashboard
|
|
236
|
+
// ═══════════════════════════════════════════════════════════════
|
|
237
|
+
|
|
238
|
+
const providers = [
|
|
239
|
+
{ name: 'nvidia', model: 'llama-3.1-8b', tier: 'free', healthy: true, latency: 85, load: 0.4 },
|
|
240
|
+
{ name: 'deepseek', model: 'v4-flash', tier: 'mid', healthy: true, latency: 210, load: 0.2 },
|
|
241
|
+
{ name: 'groq', model: '8b-instant', tier: 'cheap', healthy: true, latency: 150, load: 0.1 },
|
|
242
|
+
{ name: 'cerebras', model: '3.3-70b', tier: 'cheap', healthy: true, latency: 320, load: 0.0 },
|
|
243
|
+
{ name: 'mistral', model: 'small', tier: 'mid', healthy: false, latency: 0, load: 0 },
|
|
244
|
+
{ name: 'ollama', model: 'llama3', tier: 'local', healthy: true, latency: 50, load: 0.15 },
|
|
245
|
+
];
|
|
218
246
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
selectedFg: C.green,
|
|
222
|
-
label: ' ▸ Live Requests',
|
|
223
|
-
border: { type: 'line', fg: C.border },
|
|
224
|
-
});
|
|
247
|
+
const tierColor = (t: string) =>
|
|
248
|
+
t === 'free' ? T.green : t === 'cheap' ? T.yellow : t === 'mid' ? T.accent : t === 'local' ? T.cyan : T.accent2;
|
|
225
249
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
)
|
|
233
|
-
|
|
250
|
+
const requestLog: string[] = [];
|
|
251
|
+
|
|
252
|
+
function addLog(provider: string, model: string, latency: number, status: number) {
|
|
253
|
+
const time = new Date().toLocaleTimeString();
|
|
254
|
+
const color = status >= 400 ? `{#f7768e-fg}` : `{#9ece6a-fg}`;
|
|
255
|
+
requestLog.push(
|
|
256
|
+
`${color}${time} │ ${provider.padEnd(10)} │ ${model.padEnd(14)} │ ${String(latency).padStart(4)}ms │ ${status}{/}`
|
|
257
|
+
);
|
|
258
|
+
if (requestLog.length > 100) requestLog.shift();
|
|
234
259
|
}
|
|
235
260
|
|
|
236
|
-
|
|
237
|
-
//
|
|
238
|
-
//
|
|
261
|
+
function renderDashboard() {
|
|
262
|
+
// --- SPARKLINES ---
|
|
263
|
+
// Update history
|
|
264
|
+
costHistory.push(Math.random() * 0.05);
|
|
265
|
+
latencyHistory.push(30 + Math.random() * 250);
|
|
266
|
+
if (costHistory.length > MAX_HISTORY) costHistory.shift();
|
|
267
|
+
if (latencyHistory.length > MAX_HISTORY) latencyHistory.shift();
|
|
268
|
+
|
|
269
|
+
latencySpark.setData(['Latency'], [latencyHistory]);
|
|
270
|
+
costSpark.setData(['Cost'], [costHistory.slice(-MAX_HISTORY)]);
|
|
271
|
+
|
|
272
|
+
// --- PROVIDER HEALTH BARS ---
|
|
273
|
+
const p = pulse(tick);
|
|
274
|
+
let bars = '';
|
|
275
|
+
for (const prov of providers) {
|
|
276
|
+
const c = tierColor(prov.tier);
|
|
277
|
+
const dot = prov.healthy ? ((p > 0.5) ? '{#9ece6a-fg}●{/} ' : '{#e0af68-fg}○{/} ') : '{#f7768e-fg}✕{/} ';
|
|
278
|
+
const bar = barChart(prov.load || 0, 1, 14, ['░', '▒', '▓', '█']);
|
|
279
|
+
bars += `${dot}{bold}${prov.name.padEnd(10)}{/} {#565f89-fg}│{/} {${c}-fg}${bar}{/} {#565f89-fg}${prov.healthy ? prov.latency + 'ms' : 'OFF'}{/}\n`;
|
|
280
|
+
}
|
|
281
|
+
providerBars.setContent(bars);
|
|
239
282
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
283
|
+
// --- LIVE FEED ---
|
|
284
|
+
if (requestLog.length === 0) {
|
|
285
|
+
for (const prov of providers.filter(p => p.healthy)) {
|
|
286
|
+
addLog(prov.name, prov.model, prov.latency, 200);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
liveFeed.log('');
|
|
290
|
+
const feedSlice = requestLog.slice(-18).reverse();
|
|
291
|
+
for (const line of feedSlice) liveFeed.log(line);
|
|
292
|
+
|
|
293
|
+
// --- ROUTING MAP ---
|
|
294
|
+
routeMap.setContent([
|
|
295
|
+
` {bold} ╭── Query ──╮{/}`,
|
|
296
|
+
` {bold} ▼ ▼{/}`,
|
|
297
|
+
` ┌─────────────┐ ┌──────────┐`,
|
|
298
|
+
` │ {#7aa2f7-fg}Classifier{/} │ │ {#bb9af7-fg}Semantic{/} │`,
|
|
299
|
+
` │ {#c0caf5-fg}99.5% ±1{/} │ │ {#c0caf5-fg}Cache{/} │`,
|
|
300
|
+
` └──────┬──────┘ └────┬─────┘`,
|
|
301
|
+
` └──────┬───────┘`,
|
|
302
|
+
` ▼`,
|
|
303
|
+
` ┌─────────────┐`,
|
|
304
|
+
` │ {#7dcfff-fg}UCB1 + MCTS{/} │`,
|
|
305
|
+
` │ {#c0caf5-fg}12 Signals{/} │`,
|
|
306
|
+
` └──┬───┬───┬──┘`,
|
|
307
|
+
` {#9ece6a-fg}┌─────┘{/} │ {#f7768e-fg}└─────┐{/}`,
|
|
308
|
+
` {#9ece6a-fg}▼{/} {#e0af68-fg}▼{/} {#f7768e-fg}▼{/}`,
|
|
309
|
+
` {#9ece6a-fg}Free{/} {#e0af68-fg}Mid{/} {#f7768e-fg}Prem{/}`,
|
|
310
|
+
'',
|
|
311
|
+
` {#565f89-fg}Active: {/}{bold}nvidia{/} (free) `,
|
|
312
|
+
` {#565f89-fg}Fallback:{/} deepseek → groq`,
|
|
313
|
+
` {#565f89-fg}Cache:{/} 31.2% hit rate`,
|
|
314
|
+
].join('\n'));
|
|
315
|
+
|
|
316
|
+
// --- KPI ROW ---
|
|
317
|
+
kpiRow.setContent(
|
|
318
|
+
` {bold}{#7aa2f7-fg}⚡ ${requestLog.length}{/}{#565f89-fg} requests{/}` +
|
|
319
|
+
` │ {bold}{#9ece6a-fg}⬇ ${Math.floor(latencyHistory[latencyHistory.length-1])}{/}{#565f89-fg}ms avg{/}` +
|
|
320
|
+
` │ {bold}{#bb9af7-fg}💎 99.5%{/}{#565f89-fg} accuracy{/}` +
|
|
321
|
+
` │ {bold}{#7dcfff-fg}🖥 ${providers.filter(p=>p.healthy).length}{/}{#565f89-fg} healthy{/}` +
|
|
322
|
+
` │ {bold}{#e0af68-fg}💰 $${costHistory.reduce((a,b)=>a+b,0).toFixed(4)}{/}{#565f89-fg} spent{/}` +
|
|
323
|
+
` │ {bold}{#9ece6a-fg}📦 31.2%{/}{#565f89-fg} cache hit{/}` +
|
|
324
|
+
` │ {#565f89-fg}A3M v2.12.7{/}`
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// --- STATUS LINE ---
|
|
328
|
+
const healthy = providers.filter(p => p.healthy).length;
|
|
329
|
+
statusLine.setContent(
|
|
330
|
+
` {#9ece6a-fg}●{/} ${healthy} live │ ` +
|
|
331
|
+
`{#7dcfff-fg}↗{/} ${requestLog.length} req │ ` +
|
|
332
|
+
`{#e0af68-fg}💰{/} $${costHistory.reduce((a,b)=>a+b,0).toFixed(4)} │ ` +
|
|
333
|
+
`{#bb9af7-fg}⌛{/} ${Math.floor(latencyHistory[latencyHistory.length-1] || 0)}ms │ ` +
|
|
334
|
+
`{#565f89-fg}F1-F5 tabs / cmd q quit{/}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
246
337
|
|
|
247
|
-
|
|
338
|
+
// ═══════════════════════════════════════════════════════════════
|
|
339
|
+
// RENDER: Costs Tab
|
|
340
|
+
// ═══════════════════════════════════════════════════════════════
|
|
341
|
+
|
|
342
|
+
function renderCosts() {
|
|
248
343
|
const lines = [
|
|
249
|
-
'{
|
|
344
|
+
' {bold}{#7aa2f7-fg}╔══════════════════════════════════════╗{/}',
|
|
345
|
+
' {bold}{#7aa2f7-fg}║ COST ANALYTICS ║{/}',
|
|
346
|
+
' {bold}{#7aa2f7-fg}╚══════════════════════════════════════╝{/}',
|
|
347
|
+
'',
|
|
348
|
+
' {bold}Daily Budget: {/}{#9ece6a-fg}$5.00{/} of {#565f89-fg}$150.00{/} monthly',
|
|
250
349
|
'',
|
|
251
|
-
'
|
|
252
|
-
|
|
253
|
-
' └────┬─────┘',
|
|
254
|
-
' ▼',
|
|
255
|
-
' ┌─────────────┐',
|
|
256
|
-
' │ Classifier │',
|
|
257
|
-
' │ 99.5% ±1 │',
|
|
258
|
-
' └─────┬───────┘',
|
|
259
|
-
' ▼',
|
|
260
|
-
' ┌─────────────┐',
|
|
261
|
-
' │ 12 Signals │',
|
|
262
|
-
' │ UCB1 + MCTS │',
|
|
263
|
-
' └──┬──┬──┬───┘',
|
|
264
|
-
' │ │ │',
|
|
265
|
-
' {green-fg}┌──┘ {/} {yellow-fg}┌─┘ {/} {blue-fg}└─{/}',
|
|
266
|
-
' {green-fg}▼{/} {yellow-fg}▼{/} {blue-fg}▼{/}',
|
|
267
|
-
' {green-fg}Free{/} {yellow-fg}Mid{/} {blue-fg}Prem{/}',
|
|
350
|
+
' {bold}Today: {/}',
|
|
351
|
+
` {#e0af68-fg}░░░░░░░░░░░░░░░░░░░░░░░░{/} {#565f89-fg}0.00% used{/}`,
|
|
268
352
|
'',
|
|
269
|
-
'{
|
|
270
|
-
'{dim}Fallback: {/}deepseek/groq/cerebras',
|
|
353
|
+
' {bold}By Provider:{/}',
|
|
271
354
|
];
|
|
272
|
-
|
|
355
|
+
|
|
356
|
+
const providerCosts: Record<string, number> = {
|
|
357
|
+
nvidia: 0.000078,
|
|
358
|
+
deepseek: 0.000009,
|
|
359
|
+
groq: 0,
|
|
360
|
+
cerebras: 0,
|
|
361
|
+
ollama: 0,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
for (const [name, cost] of Object.entries(providerCosts)) {
|
|
365
|
+
const pct = Math.min((cost / 0.001) * 100, 100);
|
|
366
|
+
const bar = barChart(pct, 100, 20, ['░']);
|
|
367
|
+
const color = cost === 0 ? '#565f89' : cost < 0.0005 ? '#9ece6a' : '#e0af68';
|
|
368
|
+
lines.push(` {bold}${name.padEnd(10)}{/} {${color}-fg}${bar}{/} $${cost.toFixed(6)}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
lines.push(
|
|
372
|
+
'',
|
|
373
|
+
' {bold}Projected Monthly: {/}{#e0af68-fg}$0.0026{/}',
|
|
374
|
+
' {bold}Savings vs OpenAI: {/}{#9ece6a-fg}99.97%{/}',
|
|
375
|
+
'',
|
|
376
|
+
' {bold}Free Tier Usage:{/}',
|
|
377
|
+
' {#9ece6a-fg}NVIDIA NIM:{/} unlimited (free)',
|
|
378
|
+
' {#7dcfff-fg}Groq:{/} 14,400 req/day (free)',
|
|
379
|
+
' {#bb9af7-fg}DeepSeek:{/} $9.46 remaining',
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
costDetail.setContent(lines.join('\n'));
|
|
273
383
|
}
|
|
274
384
|
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
385
|
+
// ═══════════════════════════════════════════════════════════════
|
|
386
|
+
// RENDER: Providers Tab
|
|
387
|
+
// ═══════════════════════════════════════════════════════════════
|
|
278
388
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
389
|
+
function renderProviders() {
|
|
390
|
+
const data = providers.map(p => [
|
|
391
|
+
p.healthy ? '{#9ece6a-fg}●{/}' : '{#f7768e-fg}✕{/}',
|
|
392
|
+
p.name,
|
|
393
|
+
p.model,
|
|
394
|
+
p.tier.toUpperCase(),
|
|
395
|
+
p.healthy ? `${p.latency}ms` : 'OFFLINE',
|
|
396
|
+
p.healthy ? barChart(p.load || 0, 1, 6, ['·']) : '------',
|
|
397
|
+
`$${(p.load || 0 * 0.001).toFixed(6)}`,
|
|
398
|
+
]);
|
|
287
399
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
400
|
+
providerDetail.setData({
|
|
401
|
+
headers: ['', 'Provider', 'Model', 'Tier', 'Latency', 'Load', 'Cost'],
|
|
402
|
+
data,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ═══════════════════════════════════════════════════════════════
|
|
407
|
+
// RENDER: Logs Tab
|
|
408
|
+
// ═══════════════════════════════════════════════════════════════
|
|
409
|
+
|
|
410
|
+
function renderLogs() {
|
|
411
|
+
fullLog.log('');
|
|
412
|
+
for (const line of requestLog.slice(-50)) fullLog.log(line);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ═══════════════════════════════════════════════════════════════
|
|
416
|
+
// RENDER: Help Tab
|
|
417
|
+
// ═══════════════════════════════════════════════════════════════
|
|
418
|
+
|
|
419
|
+
function renderHelp() {
|
|
420
|
+
helpPanel.setContent([
|
|
421
|
+
'',
|
|
422
|
+
' {bold}{#7aa2f7-fg}╔══════════════════════════════════╗{/}',
|
|
423
|
+
' {bold}{#7aa2f7-fg}║ A3M ROUTER — KEYMAP ║{/}',
|
|
424
|
+
' {bold}{#7aa2f7-fg}╚══════════════════════════════════╝{/}',
|
|
425
|
+
'',
|
|
426
|
+
' {bold}FUNCTION KEYS:{/}',
|
|
427
|
+
' {#7aa2f7-fg}[F1]{/} Dashboard {#565f89-fg}Overview + sparklines{/}',
|
|
428
|
+
' {#7aa2f7-fg}[F2]{/} Costs {#565f89-fg}Budget + analytics{/}',
|
|
429
|
+
' {#7aa2f7-fg}[F3]{/} Providers {#565f89-fg}Health + load{/}',
|
|
430
|
+
' {#7aa2f7-fg}[F4]{/} Logs {#565f89-fg}Request history{/}',
|
|
431
|
+
' {#7aa2f7-fg}[F5]{/} Help {#565f89-fg}This screen{/}',
|
|
432
|
+
' {#f7768e-fg}[F10]{/} Quit',
|
|
433
|
+
'',
|
|
434
|
+
' {bold}NAVIGATION:{/}',
|
|
435
|
+
' {#7aa2f7-fg}↑↓{/} Scroll lists {#7aa2f7-fg}jk{/} Vim scroll',
|
|
436
|
+
' {#7aa2f7-fg}[tab]{/} Switch panels {#7aa2f7-fg}[enter]{/} Select',
|
|
437
|
+
'',
|
|
438
|
+
' {bold}ACTIONS:{/}',
|
|
439
|
+
' {#bb9af7-fg}[/]{/} Command mode {#bb9af7-fg}r{/} Refresh',
|
|
440
|
+
' {#bb9af7-fg}c{/} Cost view {#bb9af7-fg}q{/} Quit',
|
|
441
|
+
' {#bb9af7-fg}p{/} Provider view {#bb9af7-fg}[esc]{/} Back',
|
|
442
|
+
'',
|
|
443
|
+
' {bold}COMMANDS (press / to type):{/}',
|
|
444
|
+
' {#7dcfff-fg}/route <query>{/} Route a prompt',
|
|
445
|
+
' {#7dcfff-fg}/cost{/} Cost breakdown',
|
|
446
|
+
' {#7dcfff-fg}/health{/} Provider health',
|
|
447
|
+
' {#7dcfff-fg}/clear{/} Clear log',
|
|
448
|
+
'',
|
|
449
|
+
` {#565f89-fg}Active providers: ${providers.filter(p=>p.healthy).length} │ Requests: ${requestLog.length} │ Uptime: ${tick}s{/}`,
|
|
450
|
+
].join('\n'));
|
|
451
|
+
}
|
|
301
452
|
|
|
302
|
-
//
|
|
453
|
+
// ═══════════════════════════════════════════════════════════════
|
|
454
|
+
// TABS: Show/hide panels
|
|
455
|
+
// ═══════════════════════════════════════════════════════════════
|
|
456
|
+
|
|
457
|
+
function switchTab(tab: number) {
|
|
458
|
+
activeTab = tab;
|
|
459
|
+
|
|
460
|
+
// Hide all
|
|
461
|
+
[latencySpark, costSpark, providerBars, liveFeed, routeMap, kpiRow,
|
|
462
|
+
costDetail, providerDetail, fullLog, helpPanel].forEach(w => w.hide());
|
|
463
|
+
|
|
464
|
+
// Show active
|
|
465
|
+
if (tab === 1) {
|
|
466
|
+
latencySpark.show(); costSpark.show(); providerBars.show();
|
|
467
|
+
liveFeed.show(); routeMap.show(); kpiRow.show();
|
|
468
|
+
} else if (tab === 2) {
|
|
469
|
+
costDetail.show();
|
|
470
|
+
} else if (tab === 3) {
|
|
471
|
+
providerDetail.show();
|
|
472
|
+
} else if (tab === 4) {
|
|
473
|
+
fullLog.show();
|
|
474
|
+
} else if (tab === 5) {
|
|
475
|
+
helpPanel.show();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
renderTitleBar();
|
|
479
|
+
screen.render();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ═══════════════════════════════════════════════════════════════
|
|
303
483
|
// RENDER ALL
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
484
|
+
// ═══════════════════════════════════════════════════════════════
|
|
485
|
+
|
|
486
|
+
function fullRender() {
|
|
487
|
+
renderTitleBar();
|
|
488
|
+
renderDashboard();
|
|
489
|
+
if (activeTab === 2) renderCosts();
|
|
490
|
+
if (activeTab === 3) renderProviders();
|
|
491
|
+
if (activeTab === 4) renderLogs();
|
|
492
|
+
if (activeTab === 5) renderHelp();
|
|
312
493
|
screen.render();
|
|
313
494
|
}
|
|
314
495
|
|
|
315
|
-
//
|
|
496
|
+
// ═══════════════════════════════════════════════════════════════
|
|
316
497
|
// KEY BINDINGS
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
let activePanel: 'providers' | 'cmd' = 'providers';
|
|
498
|
+
// ═══════════════════════════════════════════════════════════════
|
|
320
499
|
|
|
321
500
|
screen.key(['q', 'C-c'], () => process.exit(0));
|
|
322
|
-
|
|
323
|
-
screen.key(['
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
501
|
+
screen.key(['f1'], () => switchTab(1));
|
|
502
|
+
screen.key(['f2'], () => switchTab(2));
|
|
503
|
+
screen.key(['f3'], () => switchTab(3));
|
|
504
|
+
screen.key(['f4'], () => switchTab(4));
|
|
505
|
+
screen.key(['f5'], () => switchTab(5));
|
|
506
|
+
screen.key(['f10'], () => process.exit(0));
|
|
507
|
+
|
|
508
|
+
screen.key(['1'], () => switchTab(1));
|
|
509
|
+
screen.key(['2'], () => switchTab(2));
|
|
510
|
+
screen.key(['3'], () => switchTab(3));
|
|
511
|
+
screen.key(['4'], () => switchTab(4));
|
|
512
|
+
screen.key(['5'], () => switchTab(5));
|
|
330
513
|
|
|
331
514
|
screen.key(['/'], () => {
|
|
332
|
-
activePanel = 'cmd';
|
|
333
515
|
cmdBar.focus();
|
|
516
|
+
cmdBar.clearValue();
|
|
334
517
|
cmdBar.readInput();
|
|
335
518
|
screen.render();
|
|
336
519
|
});
|
|
337
520
|
|
|
338
521
|
screen.key(['escape'], () => {
|
|
339
|
-
|
|
340
|
-
providerTable.focus();
|
|
522
|
+
if (activeTab === 1) switchTab(1);
|
|
341
523
|
cmdBar.clearValue();
|
|
342
524
|
screen.render();
|
|
343
525
|
});
|
|
344
526
|
|
|
345
|
-
screen.key(['
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
screen.key(['c'], () => {
|
|
353
|
-
const pct = Math.min((costSnapshot.total / 5) * 100, 100);
|
|
354
|
-
costGauge.setPercent(pct + 0.1);
|
|
355
|
-
renderAll();
|
|
527
|
+
screen.key(['r'], () => {
|
|
528
|
+
providers.forEach(p => {
|
|
529
|
+
if (p.healthy) p.latency = Math.floor(Math.random() * 200) + 30;
|
|
530
|
+
});
|
|
531
|
+
addLog('nvidia', 'llama-3.1-8b', Math.floor(Math.random() * 100) + 40, 200);
|
|
532
|
+
fullRender();
|
|
356
533
|
});
|
|
357
534
|
|
|
358
|
-
//
|
|
535
|
+
// Command handler
|
|
359
536
|
cmdBar.on('submit', (value: string) => {
|
|
360
537
|
const cmd = value.trim();
|
|
538
|
+
if (cmd) addLog('a3m', 'command', 0, 200);
|
|
361
539
|
if (cmd.startsWith('/route') || cmd.startsWith('/r ')) {
|
|
362
540
|
const query = cmd.replace(/^\/r(oute)?\s*/, '');
|
|
363
|
-
|
|
364
|
-
timestamp: new Date().toLocaleTimeString(),
|
|
365
|
-
model: 'auto',
|
|
366
|
-
provider: 'nvidia',
|
|
367
|
-
latency: Math.floor(Math.random() * 150) + 30,
|
|
368
|
-
tokens: Math.floor(Math.random() * 100),
|
|
369
|
-
cost: 0,
|
|
370
|
-
status: 200,
|
|
371
|
-
});
|
|
372
|
-
costSnapshot.requestCount++;
|
|
373
|
-
renderAll();
|
|
541
|
+
addLog('nvidia', 'auto-routed', Math.floor(Math.random() * 150) + 30, 200);
|
|
374
542
|
}
|
|
543
|
+
if (cmd === '/clear') requestLog.length = 0;
|
|
544
|
+
if (cmd === '/cost' || cmd === 'c') switchTab(2);
|
|
545
|
+
if (cmd === '/health' || cmd === 'p') switchTab(3);
|
|
546
|
+
if (cmd === '/logs' || cmd === 'l') switchTab(4);
|
|
375
547
|
cmdBar.clearValue();
|
|
376
|
-
|
|
377
|
-
|
|
548
|
+
fullRender();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// ═══════════════════════════════════════════════════════════════
|
|
552
|
+
// MOUSE SUPPORT — Click tabs
|
|
553
|
+
// ═══════════════════════════════════════════════════════════════
|
|
554
|
+
|
|
555
|
+
screen.on('mouse', (data: any) => {
|
|
556
|
+
// Mouse click on status bar triggers help
|
|
557
|
+
if (data.y === screen.height as number - 1) switchTab(5);
|
|
378
558
|
});
|
|
379
559
|
|
|
380
|
-
//
|
|
560
|
+
// ═══════════════════════════════════════════════════════════════
|
|
381
561
|
// STARTUP
|
|
382
|
-
//
|
|
562
|
+
// ═══════════════════════════════════════════════════════════════
|
|
563
|
+
|
|
564
|
+
switchTab(1);
|
|
565
|
+
|
|
566
|
+
// Initial data
|
|
567
|
+
for (let i = 0; i < 3; i++) {
|
|
568
|
+
const p = providers.filter(x => x.healthy)[Math.floor(Math.random() * 4)];
|
|
569
|
+
addLog(p.name, p.model, Math.floor(Math.random() * 100) + 40, 200);
|
|
570
|
+
costHistory.push(Math.random() * 0.03);
|
|
571
|
+
latencyHistory.push(30 + Math.random() * 200);
|
|
572
|
+
}
|
|
383
573
|
|
|
384
|
-
|
|
385
|
-
renderAll();
|
|
386
|
-
providerTable.focus();
|
|
387
|
-
screen.render();
|
|
574
|
+
fullRender();
|
|
388
575
|
|
|
389
|
-
//
|
|
576
|
+
// Live tick
|
|
390
577
|
setInterval(() => {
|
|
391
|
-
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
tokens: Math.floor(Math.random() * 100),
|
|
399
|
-
cost: Math.random() * 0.0001,
|
|
400
|
-
status: Math.random() > 0.1 ? 200 : 500,
|
|
578
|
+
tick++;
|
|
579
|
+
if (tick % 15 === 0 && tick > 0) {
|
|
580
|
+
providers.forEach(p => {
|
|
581
|
+
if (p.healthy) {
|
|
582
|
+
p.latency = Math.max(20, p.latency + (Math.random() - 0.5) * 30);
|
|
583
|
+
p.load = Math.max(0, Math.min(1, (p.load || 0) + (Math.random() - 0.5) * 0.1));
|
|
584
|
+
}
|
|
401
585
|
});
|
|
402
|
-
costSnapshot.requestCount++;
|
|
403
586
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
587
|
+
if (tick % 10 === 0 && tick > 0) {
|
|
588
|
+
const p = providers.filter(x => x.healthy)[Math.floor(Math.random() * 4)];
|
|
589
|
+
addLog(p.name, p.model, Math.floor(Math.random() * 120) + 30, Math.random() > 0.05 ? 200 : 500);
|
|
590
|
+
costHistory.push(Math.random() * 0.04);
|
|
591
|
+
latencyHistory.push(30 + Math.random() * 220);
|
|
592
|
+
if (costHistory.length > MAX_HISTORY) costHistory.shift();
|
|
593
|
+
if (latencyHistory.length > MAX_HISTORY) latencyHistory.shift();
|
|
594
|
+
}
|
|
595
|
+
fullRender();
|
|
596
|
+
}, 1000);
|