bardd commited on
Commit
dbe5f96
·
verified ·
1 Parent(s): cd16e60

Upload 140 files

Browse files
src/proxy_app/main.py CHANGED
@@ -1269,6 +1269,16 @@ async def embeddings(
1269
  raise HTTPException(status_code=500, detail=str(e))
1270
 
1271
 
 
 
 
 
 
 
 
 
 
 
1272
  @app.get("/")
1273
  def read_root():
1274
  return {"Status": "API Key Proxy is running"}
 
1269
  raise HTTPException(status_code=500, detail=str(e))
1270
 
1271
 
1272
+
1273
+ @app.get("/dashboard")
1274
+ async def dashboard():
1275
+ """Serve the usage dashboard HTML page."""
1276
+ from fastapi.responses import FileResponse
1277
+ import os
1278
+ dashboard_path = os.path.join(os.path.dirname(__file__), "static", "dashboard.html")
1279
+ return FileResponse(dashboard_path, media_type="text/html")
1280
+
1281
+
1282
  @app.get("/")
1283
  def read_root():
1284
  return {"Status": "API Key Proxy is running"}
src/proxy_app/static/dashboard.html ADDED
@@ -0,0 +1,523 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LLM Proxy Dashboard</title>
7
+ <style>
8
+ :root {
9
+ --bg-primary: #0d1117;
10
+ --bg-secondary: #161b22;
11
+ --bg-tertiary: #21262d;
12
+ --text-primary: #f0f6fc;
13
+ --text-secondary: #8b949e;
14
+ --accent-blue: #58a6ff;
15
+ --accent-green: #3fb950;
16
+ --accent-yellow: #d29922;
17
+ --accent-red: #f85149;
18
+ --accent-purple: #a371f7;
19
+ --border-color: #30363d;
20
+ }
21
+
22
+ * {
23
+ margin: 0;
24
+ padding: 0;
25
+ box-sizing: border-box;
26
+ }
27
+
28
+ body {
29
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
30
+ background: var(--bg-primary);
31
+ color: var(--text-primary);
32
+ min-height: 100vh;
33
+ padding: 20px;
34
+ }
35
+
36
+ .container {
37
+ max-width: 1200px;
38
+ margin: 0 auto;
39
+ }
40
+
41
+ header {
42
+ display: flex;
43
+ justify-content: space-between;
44
+ align-items: center;
45
+ margin-bottom: 24px;
46
+ padding-bottom: 16px;
47
+ border-bottom: 1px solid var(--border-color);
48
+ }
49
+
50
+ h1 {
51
+ font-size: 1.5rem;
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 10px;
55
+ }
56
+
57
+ .header-right {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 16px;
61
+ }
62
+
63
+ .refresh-info {
64
+ color: var(--text-secondary);
65
+ font-size: 0.85rem;
66
+ }
67
+
68
+ .refresh-btn {
69
+ background: var(--accent-blue);
70
+ color: white;
71
+ border: none;
72
+ padding: 8px 16px;
73
+ border-radius: 6px;
74
+ cursor: pointer;
75
+ font-size: 0.9rem;
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 6px;
79
+ transition: opacity 0.2s;
80
+ }
81
+
82
+ .refresh-btn:hover {
83
+ opacity: 0.9;
84
+ }
85
+
86
+ .refresh-btn:disabled {
87
+ opacity: 0.5;
88
+ cursor: not-allowed;
89
+ }
90
+
91
+ .summary-card {
92
+ background: linear-gradient(135deg, #1a1f35 0%, #141824 100%);
93
+ border: 1px solid var(--border-color);
94
+ border-radius: 12px;
95
+ padding: 24px;
96
+ margin-bottom: 24px;
97
+ }
98
+
99
+ .summary-title {
100
+ font-size: 1rem;
101
+ color: var(--text-secondary);
102
+ margin-bottom: 16px;
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 8px;
106
+ }
107
+
108
+ .summary-grid {
109
+ display: grid;
110
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
111
+ gap: 20px;
112
+ }
113
+
114
+ .stat-box {
115
+ text-align: center;
116
+ }
117
+
118
+ .stat-value {
119
+ font-size: 2rem;
120
+ font-weight: bold;
121
+ color: var(--accent-blue);
122
+ }
123
+
124
+ .stat-label {
125
+ font-size: 0.8rem;
126
+ color: var(--text-secondary);
127
+ margin-top: 4px;
128
+ }
129
+
130
+ .accounts-section {
131
+ display: grid;
132
+ gap: 16px;
133
+ }
134
+
135
+ .account-card {
136
+ background: var(--bg-secondary);
137
+ border: 1px solid var(--border-color);
138
+ border-radius: 12px;
139
+ overflow: hidden;
140
+ }
141
+
142
+ .account-header {
143
+ display: flex;
144
+ justify-content: space-between;
145
+ align-items: center;
146
+ padding: 16px 20px;
147
+ background: var(--bg-tertiary);
148
+ border-bottom: 1px solid var(--border-color);
149
+ }
150
+
151
+ .account-name {
152
+ font-weight: 600;
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 8px;
156
+ }
157
+
158
+ .account-tier {
159
+ font-size: 0.75rem;
160
+ padding: 2px 8px;
161
+ border-radius: 12px;
162
+ background: var(--accent-purple);
163
+ color: white;
164
+ }
165
+
166
+ .account-status {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 6px;
170
+ font-size: 0.85rem;
171
+ }
172
+
173
+ .status-dot {
174
+ width: 8px;
175
+ height: 8px;
176
+ border-radius: 50%;
177
+ }
178
+
179
+ .status-active { background: var(--accent-green); }
180
+ .status-cooldown { background: var(--accent-yellow); }
181
+ .status-exhausted { background: var(--accent-red); }
182
+
183
+ .account-body {
184
+ padding: 16px 20px;
185
+ }
186
+
187
+ .account-stats {
188
+ display: grid;
189
+ grid-template-columns: repeat(4, 1fr);
190
+ gap: 12px;
191
+ margin-bottom: 16px;
192
+ padding-bottom: 16px;
193
+ border-bottom: 1px solid var(--border-color);
194
+ }
195
+
196
+ .mini-stat {
197
+ text-align: center;
198
+ }
199
+
200
+ .mini-stat-value {
201
+ font-size: 1.2rem;
202
+ font-weight: bold;
203
+ color: var(--text-primary);
204
+ }
205
+
206
+ .mini-stat-label {
207
+ font-size: 0.7rem;
208
+ color: var(--text-secondary);
209
+ }
210
+
211
+ .models-list {
212
+ display: flex;
213
+ flex-direction: column;
214
+ gap: 12px;
215
+ }
216
+
217
+ .model-row {
218
+ display: grid;
219
+ grid-template-columns: 200px 1fr 100px 100px;
220
+ gap: 12px;
221
+ align-items: center;
222
+ }
223
+
224
+ .model-name {
225
+ font-size: 0.85rem;
226
+ color: var(--text-primary);
227
+ white-space: nowrap;
228
+ overflow: hidden;
229
+ text-overflow: ellipsis;
230
+ }
231
+
232
+ .progress-bar {
233
+ height: 8px;
234
+ background: var(--bg-tertiary);
235
+ border-radius: 4px;
236
+ overflow: hidden;
237
+ }
238
+
239
+ .progress-fill {
240
+ height: 100%;
241
+ border-radius: 4px;
242
+ transition: width 0.3s ease;
243
+ }
244
+
245
+ .progress-green { background: var(--accent-green); }
246
+ .progress-yellow { background: var(--accent-yellow); }
247
+ .progress-red { background: var(--accent-red); }
248
+
249
+ .quota-text {
250
+ font-size: 0.8rem;
251
+ color: var(--text-secondary);
252
+ text-align: right;
253
+ }
254
+
255
+ .reset-time {
256
+ font-size: 0.75rem;
257
+ color: var(--accent-yellow);
258
+ text-align: right;
259
+ }
260
+
261
+ .loading {
262
+ text-align: center;
263
+ padding: 60px;
264
+ color: var(--text-secondary);
265
+ }
266
+
267
+ .error-message {
268
+ background: rgba(248, 81, 73, 0.1);
269
+ border: 1px solid var(--accent-red);
270
+ color: var(--accent-red);
271
+ padding: 16px;
272
+ border-radius: 8px;
273
+ margin-bottom: 16px;
274
+ }
275
+
276
+ @media (max-width: 768px) {
277
+ .model-row {
278
+ grid-template-columns: 1fr;
279
+ gap: 4px;
280
+ }
281
+ .account-stats {
282
+ grid-template-columns: repeat(2, 1fr);
283
+ }
284
+ .summary-grid {
285
+ grid-template-columns: repeat(2, 1fr);
286
+ }
287
+ }
288
+ </style>
289
+ </head>
290
+ <body>
291
+ <div class="container">
292
+ <header>
293
+ <h1>🛡️ LLM Proxy Dashboard</h1>
294
+ <div class="header-right">
295
+ <span class="refresh-info">
296
+ Last updated: <span id="lastUpdate">--:--:--</span> •
297
+ Auto-refresh in <span id="countdown">10</span>s
298
+ </span>
299
+ <button class="refresh-btn" onclick="fetchData()" id="refreshBtn">
300
+ 🔄 Refresh
301
+ </button>
302
+ </div>
303
+ </header>
304
+
305
+ <div id="content">
306
+ <div class="loading">Loading dashboard data...</div>
307
+ </div>
308
+ </div>
309
+
310
+ <script>
311
+ const API_KEY = 'sk-antigravity-proxy-123';
312
+ let countdown = 10;
313
+ let countdownInterval;
314
+
315
+ function formatTime(date) {
316
+ return date.toLocaleTimeString('en-US', { hour12: false });
317
+ }
318
+
319
+ function formatResetTime(timestamp) {
320
+ if (!timestamp) return '--';
321
+ const resetDate = new Date(timestamp * 1000);
322
+ const now = new Date();
323
+ const diffMs = resetDate - now;
324
+
325
+ if (diffMs <= 0) return 'Now';
326
+
327
+ const hours = Math.floor(diffMs / (1000 * 60 * 60));
328
+ const mins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
329
+
330
+ if (hours > 24) {
331
+ const days = Math.floor(hours / 24);
332
+ return `${days}d ${hours % 24}h`;
333
+ }
334
+ return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
335
+ }
336
+
337
+ function getProgressColor(used, max) {
338
+ const pct = (used / max) * 100;
339
+ if (pct >= 80) return 'progress-red';
340
+ if (pct >= 50) return 'progress-yellow';
341
+ return 'progress-green';
342
+ }
343
+
344
+ function getStatusClass(status) {
345
+ if (status === 'active') return 'status-active';
346
+ if (status === 'cooldown') return 'status-cooldown';
347
+ return 'status-exhausted';
348
+ }
349
+
350
+ async function fetchData() {
351
+ const btn = document.getElementById('refreshBtn');
352
+ btn.disabled = true;
353
+ btn.innerHTML = '⏳ Loading...';
354
+
355
+ try {
356
+ const response = await fetch('/v1/quota-stats?provider=antigravity', {
357
+ headers: {
358
+ 'Authorization': `Bearer ${API_KEY}`
359
+ }
360
+ });
361
+
362
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
363
+
364
+ const data = await response.json();
365
+ renderDashboard(data);
366
+ document.getElementById('lastUpdate').textContent = formatTime(new Date());
367
+ countdown = 10;
368
+
369
+ } catch (error) {
370
+ document.getElementById('content').innerHTML = `
371
+ <div class="error-message">
372
+ ❌ Failed to fetch data: ${error.message}<br>
373
+ <small>Make sure the proxy is running and API key is correct.</small>
374
+ </div>
375
+ `;
376
+ } finally {
377
+ btn.disabled = false;
378
+ btn.innerHTML = '🔄 Refresh';
379
+ }
380
+ }
381
+
382
+ function renderDashboard(data) {
383
+ const provider = data.providers?.antigravity;
384
+ if (!provider) {
385
+ document.getElementById('content').innerHTML = '<div class="loading">No Antigravity data available</div>';
386
+ return;
387
+ }
388
+
389
+ const summary = data.summary || {};
390
+ const tokens = provider.tokens || {};
391
+ const totalTokens = (tokens.input_uncached || 0) + (tokens.input_cached || 0) + (tokens.output || 0);
392
+
393
+ let html = `
394
+ <div class="summary-card">
395
+ <div class="summary-title">📊 TOTAL USAGE</div>
396
+ <div class="summary-grid">
397
+ <div class="stat-box">
398
+ <div class="stat-value">${provider.total_requests || 0}</div>
399
+ <div class="stat-label">Requests</div>
400
+ </div>
401
+ <div class="stat-box">
402
+ <div class="stat-value">${tokens.input_uncached || 0}</div>
403
+ <div class="stat-label">Input Tokens</div>
404
+ </div>
405
+ <div class="stat-box">
406
+ <div class="stat-value">${tokens.input_cached || 0}</div>
407
+ <div class="stat-label">Cached Tokens</div>
408
+ </div>
409
+ <div class="stat-box">
410
+ <div class="stat-value">${tokens.output || 0}</div>
411
+ <div class="stat-label">Output Tokens</div>
412
+ </div>
413
+ <div class="stat-box">
414
+ <div class="stat-value">${totalTokens}</div>
415
+ <div class="stat-label">Total Tokens</div>
416
+ </div>
417
+ <div class="stat-box">
418
+ <div class="stat-value">${provider.credential_count || 0}</div>
419
+ <div class="stat-label">Accounts</div>
420
+ </div>
421
+ <div class="stat-box">
422
+ <div class="stat-value" style="color: var(--accent-green)">${provider.active_count || 0}</div>
423
+ <div class="stat-label">Active</div>
424
+ </div>
425
+ <div class="stat-box">
426
+ <div class="stat-value" style="color: var(--accent-red)">${provider.exhausted_count || 0}</div>
427
+ <div class="stat-label">Exhausted</div>
428
+ </div>
429
+ </div>
430
+ </div>
431
+
432
+ <div class="accounts-section">
433
+ `;
434
+
435
+ const credentials = provider.credentials || [];
436
+ credentials.forEach((cred, index) => {
437
+ const credTokens = cred.tokens || {};
438
+ const credTotalTokens = (credTokens.input_uncached || 0) + (credTokens.input_cached || 0) + (credTokens.output || 0);
439
+
440
+ html += `
441
+ <div class="account-card">
442
+ <div class="account-header">
443
+ <div class="account-name">
444
+ 👤 Account #${index + 1}
445
+ <span class="account-tier">${cred.tier || 'unknown'}</span>
446
+ </div>
447
+ <div class="account-status">
448
+ <span class="status-dot ${getStatusClass(cred.status)}"></span>
449
+ ${cred.status || 'unknown'}
450
+ </div>
451
+ </div>
452
+ <div class="account-body">
453
+ <div class="account-stats">
454
+ <div class="mini-stat">
455
+ <div class="mini-stat-value">${cred.requests || 0}</div>
456
+ <div class="mini-stat-label">Requests</div>
457
+ </div>
458
+ <div class="mini-stat">
459
+ <div class="mini-stat-value">${credTokens.input_uncached || 0}</div>
460
+ <div class="mini-stat-label">Input</div>
461
+ </div>
462
+ <div class="mini-stat">
463
+ <div class="mini-stat-value">${credTokens.output || 0}</div>
464
+ <div class="mini-stat-label">Output</div>
465
+ </div>
466
+ <div class="mini-stat">
467
+ <div class="mini-stat-value">${credTotalTokens}</div>
468
+ <div class="mini-stat-label">Total</div>
469
+ </div>
470
+ </div>
471
+ <div class="models-list">
472
+ `;
473
+
474
+ // Render model groups instead of individual models for cleaner view
475
+ const groups = cred.model_groups || {};
476
+ Object.entries(groups).forEach(([groupName, group]) => {
477
+ const used = group.requests_used || 0;
478
+ const max = group.requests_max || 1;
479
+ const remaining = group.requests_remaining || 0;
480
+ const pct = (used / max) * 100;
481
+ const resetTime = group.reset_time_iso ? new Date(group.reset_time_iso).getTime() / 1000 : null;
482
+
483
+ html += `
484
+ <div class="model-row">
485
+ <div class="model-name">${groupName}</div>
486
+ <div class="progress-bar">
487
+ <div class="progress-fill ${getProgressColor(used, max)}" style="width: ${pct}%"></div>
488
+ </div>
489
+ <div class="quota-text">${used}/${max}</div>
490
+ <div class="reset-time">${formatResetTime(resetTime)}</div>
491
+ </div>
492
+ `;
493
+ });
494
+
495
+ html += `
496
+ </div>
497
+ </div>
498
+ </div>
499
+ `;
500
+ });
501
+
502
+ html += '</div>';
503
+ document.getElementById('content').innerHTML = html;
504
+ }
505
+
506
+ function startCountdown() {
507
+ countdownInterval = setInterval(() => {
508
+ countdown--;
509
+ document.getElementById('countdown').textContent = countdown;
510
+
511
+ if (countdown <= 0) {
512
+ countdown = 10;
513
+ fetchData();
514
+ }
515
+ }, 1000);
516
+ }
517
+
518
+ // Initial load
519
+ fetchData();
520
+ startCountdown();
521
+ </script>
522
+ </body>
523
+ </html>