ocb-cli 1.0.1 → 1.0.2
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/dist/proxy.js +181 -35
- package/package.json +1 -1
- package/src/proxy.ts +182 -35
package/dist/proxy.js
CHANGED
|
@@ -115,6 +115,9 @@ app.use((req, res, next) => {
|
|
|
115
115
|
next();
|
|
116
116
|
});
|
|
117
117
|
app.get("/", (req, res) => res.send(generateHTML()));
|
|
118
|
+
app.get("/health", (req, res) => {
|
|
119
|
+
res.json({ status: "ok", timestamp: Date.now(), models: availableModels.length, providers: Object.keys(providers).length });
|
|
120
|
+
});
|
|
118
121
|
app.get("/api/status", (req, res) => {
|
|
119
122
|
res.json({
|
|
120
123
|
proxyPort: PROXY_PORT,
|
|
@@ -198,58 +201,166 @@ function generateHTML() {
|
|
|
198
201
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
199
202
|
<title>OCB - OpenCode Bridge</title>
|
|
200
203
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
201
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
204
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
202
205
|
<style>
|
|
203
|
-
* {
|
|
204
|
-
body { font-family: 'Inter', -apple-system, sans-serif; background: #
|
|
206
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
207
|
+
body { font-family: 'Inter', -apple-system, sans-serif; background: #0f0f11; color: #fafafa; min-height: 100vh; }
|
|
205
208
|
::-webkit-scrollbar { width: 6px; }
|
|
206
209
|
::-webkit-scrollbar-track { background: transparent; }
|
|
207
210
|
::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
|
|
211
|
+
.tab-active { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; }
|
|
212
|
+
.model-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px -5px rgba(99, 102, 241, 0.3); }
|
|
208
213
|
</style>
|
|
209
214
|
</head>
|
|
210
215
|
<body>
|
|
211
216
|
<div class="min-h-screen flex flex-col">
|
|
212
|
-
<header class="bg-
|
|
217
|
+
<header class="bg-[#18181b] border-b border-zinc-800 px-6 py-4">
|
|
213
218
|
<div class="max-w-7xl mx-auto flex items-center justify-between">
|
|
214
|
-
<div class="flex items-center gap-
|
|
215
|
-
<div class="w-
|
|
219
|
+
<div class="flex items-center gap-4">
|
|
220
|
+
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center text-2xl shadow-lg shadow-indigo-500/20">⚡</div>
|
|
216
221
|
<div>
|
|
217
|
-
<h1 class="text-
|
|
218
|
-
<p class="text-xs text-zinc-500">OpenCode Bridge</p>
|
|
222
|
+
<h1 class="text-2xl font-bold bg-gradient-to-r from-white to-zinc-400 bg-clip-text text-transparent">OCB</h1>
|
|
223
|
+
<p class="text-xs text-zinc-500 font-medium">OpenCode Bridge</p>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="ml-8 flex items-center gap-2 px-4 py-2 bg-zinc-900 rounded-xl border border-zinc-800">
|
|
226
|
+
<div id="healthDot" class="w-2.5 h-2.5 bg-green-500 rounded-full animate-pulse"></div>
|
|
227
|
+
<span id="connectionStatus" class="text-sm text-zinc-300">Connected</span>
|
|
219
228
|
</div>
|
|
220
229
|
</div>
|
|
221
230
|
<div class="flex items-center gap-3">
|
|
222
|
-
<div class="
|
|
223
|
-
<
|
|
224
|
-
<
|
|
231
|
+
<div class="text-right mr-4">
|
|
232
|
+
<p class="text-xs text-zinc-500">Current Model</p>
|
|
233
|
+
<p id="currentModelDisplay" class="text-sm font-medium text-white">Loading...</p>
|
|
225
234
|
</div>
|
|
226
|
-
<button onclick="refreshModels()" class="
|
|
227
|
-
|
|
235
|
+
<button onclick="refreshModels()" class="p-2.5 bg-zinc-800 hover:bg-zinc-700 rounded-xl transition text-zinc-300 hover:text-white" title="Refresh Models">
|
|
236
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
|
|
237
|
+
</button>
|
|
228
238
|
</div>
|
|
229
239
|
</div>
|
|
230
240
|
</header>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
241
|
+
|
|
242
|
+
<nav class="bg-[#18181b] border-b border-zinc-800 px-6">
|
|
243
|
+
<div class="max-w-7xl mx-auto flex gap-1">
|
|
244
|
+
<button onclick="switchTab('models')" id="tab-models" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all tab-active">Models</button>
|
|
245
|
+
<button onclick="switchTab('status')" id="tab-status" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Status</button>
|
|
246
|
+
<button onclick="switchTab('settings')" id="tab-settings" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Settings</button>
|
|
247
|
+
</div>
|
|
248
|
+
</nav>
|
|
249
|
+
|
|
250
|
+
<main class="flex-1 max-w-7xl mx-auto w-full p-6">
|
|
251
|
+
<div id="models-view" class="flex gap-6 h-[calc(100vh-220px)]">
|
|
252
|
+
<aside class="w-72 bg-[#18181b] rounded-2xl border border-zinc-800 flex flex-col overflow-hidden">
|
|
253
|
+
<div class="p-4 border-b border-zinc-800">
|
|
254
|
+
<div class="relative">
|
|
255
|
+
<input type="text" id="searchInput" placeholder="Search models..." oninput="filterData()" class="w-full px-4 py-2.5 bg-zinc-900 border border-zinc-700 rounded-xl text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20">
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
<div id="providerList" class="flex-1 overflow-y-auto p-2"></div>
|
|
259
|
+
</aside>
|
|
260
|
+
<section class="flex-1 overflow-y-auto">
|
|
261
|
+
<div class="flex items-center justify-between mb-4">
|
|
262
|
+
<h2 id="sectionTitle" class="text-lg font-semibold text-white">All Models</h2>
|
|
263
|
+
<span id="modelCount" class="text-sm text-zinc-500"></span>
|
|
264
|
+
</div>
|
|
265
|
+
<div id="modelGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></div>
|
|
266
|
+
</section>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div id="status-view" class="hidden space-y-6">
|
|
270
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
271
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
272
|
+
<div class="flex items-center gap-3 mb-4">
|
|
273
|
+
<div class="w-10 h-10 bg-indigo-500/20 rounded-xl flex items-center justify-center text-indigo-400">📊</div>
|
|
274
|
+
<div>
|
|
275
|
+
<p class="text-xs text-zinc-500">Total Requests</p>
|
|
276
|
+
<p id="totalRequests" class="text-2xl font-bold text-white">0</p>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
281
|
+
<div class="flex items-center gap-3 mb-4">
|
|
282
|
+
<div class="w-10 h-10 bg-purple-500/20 rounded-xl flex items-center justify-center text-purple-400">🎯</div>
|
|
283
|
+
<div>
|
|
284
|
+
<p class="text-xs text-zinc-500">Total Tokens</p>
|
|
285
|
+
<p id="totalTokens" class="text-2xl font-bold text-white">0</p>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
290
|
+
<div class="flex items-center gap-3 mb-4">
|
|
291
|
+
<div class="w-10 h-10 bg-green-500/20 rounded-xl flex items-center justify-center text-green-400">🔗</div>
|
|
292
|
+
<div>
|
|
293
|
+
<p class="text-xs text-zinc-500">Session</p>
|
|
294
|
+
<p id="sessionStatus" class="text-lg font-semibold text-green-400">Active</p>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
300
|
+
<h3 class="text-lg font-semibold text-white mb-4">Model Usage</h3>
|
|
301
|
+
<div class="space-y-3">
|
|
302
|
+
<div class="flex justify-between items-center py-2 border-b border-zinc-800">
|
|
303
|
+
<span class="text-zinc-400">Active Model</span>
|
|
304
|
+
<span id="activeModelName" class="text-white font-medium">-</span>
|
|
305
|
+
</div>
|
|
306
|
+
<div class="flex justify-between items-center py-2 border-b border-zinc-800">
|
|
307
|
+
<span class="text-zinc-400">Available Models</span>
|
|
308
|
+
<span id="totalModelsDisplay" class="text-white font-medium">-</span>
|
|
309
|
+
</div>
|
|
310
|
+
<div class="flex justify-between items-center py-2 border-b border-zinc-800">
|
|
311
|
+
<span class="text-zinc-400">Providers</span>
|
|
312
|
+
<span id="totalProviders" class="text-white font-medium">-</span>
|
|
313
|
+
</div>
|
|
314
|
+
<div class="flex justify-between items-center py-2">
|
|
315
|
+
<span class="text-zinc-400">OpenCode Server</span>
|
|
316
|
+
<span class="text-green-400 font-medium">localhost:4096</span>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="flex gap-4">
|
|
321
|
+
<button onclick="resetSession()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Session</button>
|
|
322
|
+
<button onclick="resetStats()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Stats</button>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div id="settings-view" class="hidden space-y-6">
|
|
327
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
328
|
+
<h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
|
|
329
|
+
<p class="text-zinc-400 text-sm mb-4">Add this to your Claude Code settings to use OCB:</p>
|
|
330
|
+
<div class="bg-zinc-950 rounded-xl p-4 font-mono text-sm text-zinc-300 overflow-x-auto">
|
|
331
|
+
<pre id="configJson">{
|
|
332
|
+
"env": {
|
|
333
|
+
"ANTHROPIC_BASE_URL": "http://localhost:8300/v1",
|
|
334
|
+
"ANTHROPIC_AUTH_TOKEN": "test",
|
|
335
|
+
"ANTHROPIC_MODEL": "minimax-m2.5-free"
|
|
336
|
+
}
|
|
337
|
+
}</pre>
|
|
338
|
+
</div>
|
|
339
|
+
<button onclick="copyConfig()" class="mt-4 px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 rounded-xl text-white font-medium transition flex items-center gap-2">
|
|
340
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
|
341
|
+
Copy Config
|
|
342
|
+
</button>
|
|
235
343
|
</div>
|
|
236
|
-
<div
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
344
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
345
|
+
<h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
|
|
346
|
+
<div class="space-y-2 font-mono text-sm">
|
|
347
|
+
<div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Health</span><span class="text-indigo-400">http://localhost:8300/health</span></div>
|
|
348
|
+
<div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Status</span><span class="text-indigo-400">http://localhost:8300/api/status</span></div>
|
|
349
|
+
<div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Models</span><span class="text-indigo-400">http://localhost:8300/api/models</span></div>
|
|
350
|
+
<div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Set Model</span><span class="text-indigo-400">POST /api/model</span></div>
|
|
351
|
+
<div class="flex justify-between py-2"><span class="text-zinc-400">Messages</span><span class="text-indigo-400">http://localhost:8300/v1/messages</span></div>
|
|
352
|
+
</div>
|
|
242
353
|
</div>
|
|
243
|
-
|
|
244
|
-
</section>
|
|
354
|
+
</div>
|
|
245
355
|
</main>
|
|
246
|
-
|
|
356
|
+
|
|
357
|
+
<footer class="bg-[#18181b] border-t border-zinc-800 px-6 py-3">
|
|
247
358
|
<div class="max-w-7xl mx-auto flex items-center justify-between text-sm text-zinc-500">
|
|
248
359
|
<div class="flex items-center gap-6">
|
|
249
|
-
<span>
|
|
250
|
-
<span>
|
|
360
|
+
<span>OCB v1.0.1</span>
|
|
361
|
+
<span>Proxy Port: <span class="text-white">8300</span></span>
|
|
251
362
|
</div>
|
|
252
|
-
<span
|
|
363
|
+
<span>OpenCode Bridge for Claude Code</span>
|
|
253
364
|
</div>
|
|
254
365
|
</footer>
|
|
255
366
|
</div>
|
|
@@ -259,6 +370,31 @@ function generateHTML() {
|
|
|
259
370
|
let currentProvider = 'all';
|
|
260
371
|
let currentModelId = null;
|
|
261
372
|
|
|
373
|
+
function switchTab(tab) {
|
|
374
|
+
document.querySelectorAll('[id^="tab-"]').forEach(el => {
|
|
375
|
+
el.classList.remove('tab-active');
|
|
376
|
+
el.classList.add('text-zinc-400', 'hover:bg-zinc-800');
|
|
377
|
+
});
|
|
378
|
+
document.getElementById('tab-' + tab).classList.add('tab-active');
|
|
379
|
+
document.getElementById('tab-' + tab).classList.remove('text-zinc-400', 'hover:bg-zinc-800');
|
|
380
|
+
|
|
381
|
+
document.getElementById('models-view').classList.add('hidden');
|
|
382
|
+
document.getElementById('status-view').classList.add('hidden');
|
|
383
|
+
document.getElementById('settings-view').classList.add('hidden');
|
|
384
|
+
document.getElementById(tab + '-view').classList.remove('hidden');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function copyConfig() {
|
|
388
|
+
const config = document.getElementById('configJson').textContent;
|
|
389
|
+
navigator.clipboard.writeText(config);
|
|
390
|
+
alert('Config copied to clipboard!');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function resetStats() {
|
|
394
|
+
await fetch('/api/reset-stats', { method: 'POST' });
|
|
395
|
+
loadStatus();
|
|
396
|
+
}
|
|
397
|
+
|
|
262
398
|
async function loadModels() {
|
|
263
399
|
const res = await fetch('/api/models');
|
|
264
400
|
const data = await res.json();
|
|
@@ -276,9 +412,9 @@ function generateHTML() {
|
|
|
276
412
|
if (searchTerm) {
|
|
277
413
|
providersToShow = providersToShow.filter(([name, models]) => name.toLowerCase().includes(searchTerm) || models.some(m => m.name.toLowerCase().includes(searchTerm)));
|
|
278
414
|
}
|
|
279
|
-
let html = '<div onclick="selectProvider('all')" class="px-3 py-2 rounded-
|
|
415
|
+
let html = '<div onclick="selectProvider('all')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === 'all' ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span>🏠 All Models</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + allModels.length + '</span></div>';
|
|
280
416
|
for (const [provider, models] of providersToShow) {
|
|
281
|
-
html += '<div onclick="selectProvider(\\'' + provider + '\\')" class="px-3 py-2 rounded-
|
|
417
|
+
html += '<div onclick="selectProvider(\\'' + provider.replace(/'/g, "\\\\'") + '\\')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === provider ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span class="truncate">' + provider + '</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + models.length + '</span></div>';
|
|
282
418
|
}
|
|
283
419
|
container.innerHTML = html;
|
|
284
420
|
}
|
|
@@ -302,14 +438,14 @@ function generateHTML() {
|
|
|
302
438
|
container.innerHTML = '<div class="col-span-full text-center text-zinc-500 py-12">No models found</div>';
|
|
303
439
|
return;
|
|
304
440
|
}
|
|
305
|
-
container.innerHTML = filtered.map(m => '<div onclick="selectModel(\\'' + m.id + '\\')" class="p-
|
|
441
|
+
container.innerHTML = filtered.slice(0, 100).map(m => '<div onclick="selectModel(\\'' + m.id.replace(/'/g, "\\\\'") + '\\')" class="model-card p-5 rounded-2xl border cursor-pointer transition-all ' + (m.id === currentModelId ? 'bg-indigo-600/20 border-indigo-500 shadow-lg shadow-indigo-500/20' : 'bg-[#18181b] border-zinc-800 hover:border-zinc-700') + '"><div class="flex items-start justify-between mb-3"><h3 class="font-semibold text-white truncate text-base">' + m.name + '</h3>' + (m.id === currentModelId ? '<span class="text-xs bg-indigo-500 text-white px-3 py-1 rounded-full font-medium">Active</span>' : '') + '</div><p class="text-xs text-zinc-500 font-mono truncate mb-3">' + m.id + '</p>' + (m.cost ? '<div class="flex items-center gap-2 text-xs text-zinc-400"><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.input + '/M</span><span>→</span><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.output + '/M</span></div>' : '<div class="text-xs text-zinc-600">Free</div>') + '</div>').join('');
|
|
306
442
|
}
|
|
307
443
|
|
|
308
444
|
function filterData() {
|
|
309
445
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
310
446
|
if (searchTerm) {
|
|
311
447
|
const filtered = allModels.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
|
|
312
|
-
document.getElementById('sectionTitle').textContent = 'Search Results';
|
|
448
|
+
document.getElementById('sectionTitle').textContent = 'Search Results (' + filtered.length + ')';
|
|
313
449
|
renderModels(filtered);
|
|
314
450
|
renderProviders();
|
|
315
451
|
} else {
|
|
@@ -324,6 +460,7 @@ function generateHTML() {
|
|
|
324
460
|
currentModelId = modelId;
|
|
325
461
|
loadStatus();
|
|
326
462
|
renderModels(currentProvider === 'all' ? allModels : (groupedModels[currentProvider] || []));
|
|
463
|
+
document.getElementById('configJson').textContent = JSON.stringify({ env: { ANTHROPIC_BASE_URL: "http://localhost:8300/v1", ANTHROPIC_AUTH_TOKEN: "test", ANTHROPIC_MODEL: modelId } }, null, 2);
|
|
327
464
|
}
|
|
328
465
|
}
|
|
329
466
|
|
|
@@ -332,10 +469,15 @@ function generateHTML() {
|
|
|
332
469
|
const data = await res.json();
|
|
333
470
|
const model = allModels.find(m => m.id === data.currentModel);
|
|
334
471
|
document.getElementById('currentModelDisplay').textContent = model?.name || data.currentModel;
|
|
335
|
-
document.getElementById('
|
|
472
|
+
document.getElementById('healthDot').className = 'w-2.5 h-2.5 rounded-full animate-pulse ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
|
|
473
|
+
document.getElementById('connectionStatus').textContent = data.sessionId === 'active' ? 'Connected' : 'Disconnected';
|
|
336
474
|
document.getElementById('totalRequests').textContent = data.totalRequests.toLocaleString();
|
|
337
475
|
document.getElementById('totalTokens').textContent = data.totalTokensUsed.toLocaleString();
|
|
338
|
-
document.getElementById('
|
|
476
|
+
document.getElementById('sessionStatus').textContent = data.sessionId === 'active' ? 'Active' : 'Inactive';
|
|
477
|
+
document.getElementById('sessionStatus').className = 'text-lg font-semibold ' + (data.sessionId === 'active' ? 'text-green-400' : 'text-red-400');
|
|
478
|
+
document.getElementById('activeModelName').textContent = model?.name || data.currentModel;
|
|
479
|
+
document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models';
|
|
480
|
+
document.getElementById('totalProviders').textContent = Object.keys(groupedModels).length + ' providers';
|
|
339
481
|
currentModelId = data.currentModel;
|
|
340
482
|
}
|
|
341
483
|
|
|
@@ -349,6 +491,10 @@ function generateHTML() {
|
|
|
349
491
|
loadModels();
|
|
350
492
|
}
|
|
351
493
|
|
|
494
|
+
function updateStats() {
|
|
495
|
+
document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
|
|
496
|
+
}
|
|
497
|
+
|
|
352
498
|
loadModels();
|
|
353
499
|
loadStatus();
|
|
354
500
|
setInterval(loadStatus, 3000);
|
package/package.json
CHANGED
package/src/proxy.ts
CHANGED
|
@@ -143,6 +143,10 @@ app.use((req, res, next) => {
|
|
|
143
143
|
|
|
144
144
|
app.get("/", (req, res) => res.send(generateHTML()));
|
|
145
145
|
|
|
146
|
+
app.get("/health", (req, res) => {
|
|
147
|
+
res.json({ status: "ok", timestamp: Date.now(), models: availableModels.length, providers: Object.keys(providers).length });
|
|
148
|
+
});
|
|
149
|
+
|
|
146
150
|
app.get("/api/status", (req, res) => {
|
|
147
151
|
res.json({
|
|
148
152
|
proxyPort: PROXY_PORT,
|
|
@@ -232,58 +236,166 @@ function generateHTML() {
|
|
|
232
236
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
233
237
|
<title>OCB - OpenCode Bridge</title>
|
|
234
238
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
235
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
239
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
236
240
|
<style>
|
|
237
|
-
* {
|
|
238
|
-
body { font-family: 'Inter', -apple-system, sans-serif; background: #
|
|
241
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
242
|
+
body { font-family: 'Inter', -apple-system, sans-serif; background: #0f0f11; color: #fafafa; min-height: 100vh; }
|
|
239
243
|
::-webkit-scrollbar { width: 6px; }
|
|
240
244
|
::-webkit-scrollbar-track { background: transparent; }
|
|
241
245
|
::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
|
|
246
|
+
.tab-active { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; }
|
|
247
|
+
.model-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px -5px rgba(99, 102, 241, 0.3); }
|
|
242
248
|
</style>
|
|
243
249
|
</head>
|
|
244
250
|
<body>
|
|
245
251
|
<div class="min-h-screen flex flex-col">
|
|
246
|
-
<header class="bg-
|
|
252
|
+
<header class="bg-[#18181b] border-b border-zinc-800 px-6 py-4">
|
|
247
253
|
<div class="max-w-7xl mx-auto flex items-center justify-between">
|
|
248
|
-
<div class="flex items-center gap-
|
|
249
|
-
<div class="w-
|
|
254
|
+
<div class="flex items-center gap-4">
|
|
255
|
+
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center text-2xl shadow-lg shadow-indigo-500/20">⚡</div>
|
|
250
256
|
<div>
|
|
251
|
-
<h1 class="text-
|
|
252
|
-
<p class="text-xs text-zinc-500">OpenCode Bridge</p>
|
|
257
|
+
<h1 class="text-2xl font-bold bg-gradient-to-r from-white to-zinc-400 bg-clip-text text-transparent">OCB</h1>
|
|
258
|
+
<p class="text-xs text-zinc-500 font-medium">OpenCode Bridge</p>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="ml-8 flex items-center gap-2 px-4 py-2 bg-zinc-900 rounded-xl border border-zinc-800">
|
|
261
|
+
<div id="healthDot" class="w-2.5 h-2.5 bg-green-500 rounded-full animate-pulse"></div>
|
|
262
|
+
<span id="connectionStatus" class="text-sm text-zinc-300">Connected</span>
|
|
253
263
|
</div>
|
|
254
264
|
</div>
|
|
255
265
|
<div class="flex items-center gap-3">
|
|
256
|
-
<div class="
|
|
257
|
-
<
|
|
258
|
-
<
|
|
266
|
+
<div class="text-right mr-4">
|
|
267
|
+
<p class="text-xs text-zinc-500">Current Model</p>
|
|
268
|
+
<p id="currentModelDisplay" class="text-sm font-medium text-white">Loading...</p>
|
|
259
269
|
</div>
|
|
260
|
-
<button onclick="refreshModels()" class="
|
|
261
|
-
|
|
270
|
+
<button onclick="refreshModels()" class="p-2.5 bg-zinc-800 hover:bg-zinc-700 rounded-xl transition text-zinc-300 hover:text-white" title="Refresh Models">
|
|
271
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
|
|
272
|
+
</button>
|
|
262
273
|
</div>
|
|
263
274
|
</div>
|
|
264
275
|
</header>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
276
|
+
|
|
277
|
+
<nav class="bg-[#18181b] border-b border-zinc-800 px-6">
|
|
278
|
+
<div class="max-w-7xl mx-auto flex gap-1">
|
|
279
|
+
<button onclick="switchTab('models')" id="tab-models" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all tab-active">Models</button>
|
|
280
|
+
<button onclick="switchTab('status')" id="tab-status" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Status</button>
|
|
281
|
+
<button onclick="switchTab('settings')" id="tab-settings" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Settings</button>
|
|
282
|
+
</div>
|
|
283
|
+
</nav>
|
|
284
|
+
|
|
285
|
+
<main class="flex-1 max-w-7xl mx-auto w-full p-6">
|
|
286
|
+
<div id="models-view" class="flex gap-6 h-[calc(100vh-220px)]">
|
|
287
|
+
<aside class="w-72 bg-[#18181b] rounded-2xl border border-zinc-800 flex flex-col overflow-hidden">
|
|
288
|
+
<div class="p-4 border-b border-zinc-800">
|
|
289
|
+
<div class="relative">
|
|
290
|
+
<input type="text" id="searchInput" placeholder="Search models..." oninput="filterData()" class="w-full px-4 py-2.5 bg-zinc-900 border border-zinc-700 rounded-xl text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20">
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
<div id="providerList" class="flex-1 overflow-y-auto p-2"></div>
|
|
294
|
+
</aside>
|
|
295
|
+
<section class="flex-1 overflow-y-auto">
|
|
296
|
+
<div class="flex items-center justify-between mb-4">
|
|
297
|
+
<h2 id="sectionTitle" class="text-lg font-semibold text-white">All Models</h2>
|
|
298
|
+
<span id="modelCount" class="text-sm text-zinc-500"></span>
|
|
299
|
+
</div>
|
|
300
|
+
<div id="modelGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></div>
|
|
301
|
+
</section>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div id="status-view" class="hidden space-y-6">
|
|
305
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
306
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
307
|
+
<div class="flex items-center gap-3 mb-4">
|
|
308
|
+
<div class="w-10 h-10 bg-indigo-500/20 rounded-xl flex items-center justify-center text-indigo-400">📊</div>
|
|
309
|
+
<div>
|
|
310
|
+
<p class="text-xs text-zinc-500">Total Requests</p>
|
|
311
|
+
<p id="totalRequests" class="text-2xl font-bold text-white">0</p>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
316
|
+
<div class="flex items-center gap-3 mb-4">
|
|
317
|
+
<div class="w-10 h-10 bg-purple-500/20 rounded-xl flex items-center justify-center text-purple-400">🎯</div>
|
|
318
|
+
<div>
|
|
319
|
+
<p class="text-xs text-zinc-500">Total Tokens</p>
|
|
320
|
+
<p id="totalTokens" class="text-2xl font-bold text-white">0</p>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
325
|
+
<div class="flex items-center gap-3 mb-4">
|
|
326
|
+
<div class="w-10 h-10 bg-green-500/20 rounded-xl flex items-center justify-center text-green-400">🔗</div>
|
|
327
|
+
<div>
|
|
328
|
+
<p class="text-xs text-zinc-500">Session</p>
|
|
329
|
+
<p id="sessionStatus" class="text-lg font-semibold text-green-400">Active</p>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
335
|
+
<h3 class="text-lg font-semibold text-white mb-4">Model Usage</h3>
|
|
336
|
+
<div class="space-y-3">
|
|
337
|
+
<div class="flex justify-between items-center py-2 border-b border-zinc-800">
|
|
338
|
+
<span class="text-zinc-400">Active Model</span>
|
|
339
|
+
<span id="activeModelName" class="text-white font-medium">-</span>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="flex justify-between items-center py-2 border-b border-zinc-800">
|
|
342
|
+
<span class="text-zinc-400">Available Models</span>
|
|
343
|
+
<span id="totalModelsDisplay" class="text-white font-medium">-</span>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="flex justify-between items-center py-2 border-b border-zinc-800">
|
|
346
|
+
<span class="text-zinc-400">Providers</span>
|
|
347
|
+
<span id="totalProviders" class="text-white font-medium">-</span>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="flex justify-between items-center py-2">
|
|
350
|
+
<span class="text-zinc-400">OpenCode Server</span>
|
|
351
|
+
<span class="text-green-400 font-medium">localhost:4096</span>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
269
354
|
</div>
|
|
270
|
-
<div
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
<div class="flex items-center justify-between mb-4">
|
|
274
|
-
<h2 id="sectionTitle" class="text-lg font-medium text-white">All Models</h2>
|
|
275
|
-
<span id="modelCount" class="text-sm text-zinc-500"></span>
|
|
355
|
+
<div class="flex gap-4">
|
|
356
|
+
<button onclick="resetSession()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Session</button>
|
|
357
|
+
<button onclick="resetStats()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Stats</button>
|
|
276
358
|
</div>
|
|
277
|
-
|
|
278
|
-
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<div id="settings-view" class="hidden space-y-6">
|
|
362
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
363
|
+
<h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
|
|
364
|
+
<p class="text-zinc-400 text-sm mb-4">Add this to your Claude Code settings to use OCB:</p>
|
|
365
|
+
<div class="bg-zinc-950 rounded-xl p-4 font-mono text-sm text-zinc-300 overflow-x-auto">
|
|
366
|
+
<pre id="configJson">{
|
|
367
|
+
"env": {
|
|
368
|
+
"ANTHROPIC_BASE_URL": "http://localhost:8300/v1",
|
|
369
|
+
"ANTHROPIC_AUTH_TOKEN": "test",
|
|
370
|
+
"ANTHROPIC_MODEL": "minimax-m2.5-free"
|
|
371
|
+
}
|
|
372
|
+
}</pre>
|
|
373
|
+
</div>
|
|
374
|
+
<button onclick="copyConfig()" class="mt-4 px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 rounded-xl text-white font-medium transition flex items-center gap-2">
|
|
375
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
|
376
|
+
Copy Config
|
|
377
|
+
</button>
|
|
378
|
+
</div>
|
|
379
|
+
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
380
|
+
<h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
|
|
381
|
+
<div class="space-y-2 font-mono text-sm">
|
|
382
|
+
<div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Health</span><span class="text-indigo-400">http://localhost:8300/health</span></div>
|
|
383
|
+
<div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Status</span><span class="text-indigo-400">http://localhost:8300/api/status</span></div>
|
|
384
|
+
<div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Models</span><span class="text-indigo-400">http://localhost:8300/api/models</span></div>
|
|
385
|
+
<div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Set Model</span><span class="text-indigo-400">POST /api/model</span></div>
|
|
386
|
+
<div class="flex justify-between py-2"><span class="text-zinc-400">Messages</span><span class="text-indigo-400">http://localhost:8300/v1/messages</span></div>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
279
390
|
</main>
|
|
280
|
-
|
|
391
|
+
|
|
392
|
+
<footer class="bg-[#18181b] border-t border-zinc-800 px-6 py-3">
|
|
281
393
|
<div class="max-w-7xl mx-auto flex items-center justify-between text-sm text-zinc-500">
|
|
282
394
|
<div class="flex items-center gap-6">
|
|
283
|
-
<span>
|
|
284
|
-
<span>
|
|
395
|
+
<span>OCB v1.0.1</span>
|
|
396
|
+
<span>Proxy Port: <span class="text-white">8300</span></span>
|
|
285
397
|
</div>
|
|
286
|
-
<span
|
|
398
|
+
<span>OpenCode Bridge for Claude Code</span>
|
|
287
399
|
</div>
|
|
288
400
|
</footer>
|
|
289
401
|
</div>
|
|
@@ -293,6 +405,31 @@ function generateHTML() {
|
|
|
293
405
|
let currentProvider = 'all';
|
|
294
406
|
let currentModelId = null;
|
|
295
407
|
|
|
408
|
+
function switchTab(tab) {
|
|
409
|
+
document.querySelectorAll('[id^="tab-"]').forEach(el => {
|
|
410
|
+
el.classList.remove('tab-active');
|
|
411
|
+
el.classList.add('text-zinc-400', 'hover:bg-zinc-800');
|
|
412
|
+
});
|
|
413
|
+
document.getElementById('tab-' + tab).classList.add('tab-active');
|
|
414
|
+
document.getElementById('tab-' + tab).classList.remove('text-zinc-400', 'hover:bg-zinc-800');
|
|
415
|
+
|
|
416
|
+
document.getElementById('models-view').classList.add('hidden');
|
|
417
|
+
document.getElementById('status-view').classList.add('hidden');
|
|
418
|
+
document.getElementById('settings-view').classList.add('hidden');
|
|
419
|
+
document.getElementById(tab + '-view').classList.remove('hidden');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function copyConfig() {
|
|
423
|
+
const config = document.getElementById('configJson').textContent;
|
|
424
|
+
navigator.clipboard.writeText(config);
|
|
425
|
+
alert('Config copied to clipboard!');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function resetStats() {
|
|
429
|
+
await fetch('/api/reset-stats', { method: 'POST' });
|
|
430
|
+
loadStatus();
|
|
431
|
+
}
|
|
432
|
+
|
|
296
433
|
async function loadModels() {
|
|
297
434
|
const res = await fetch('/api/models');
|
|
298
435
|
const data = await res.json();
|
|
@@ -310,9 +447,9 @@ function generateHTML() {
|
|
|
310
447
|
if (searchTerm) {
|
|
311
448
|
providersToShow = providersToShow.filter(([name, models]) => name.toLowerCase().includes(searchTerm) || models.some(m => m.name.toLowerCase().includes(searchTerm)));
|
|
312
449
|
}
|
|
313
|
-
let html = '<div onclick="selectProvider('all')" class="px-3 py-2 rounded-
|
|
450
|
+
let html = '<div onclick="selectProvider('all')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === 'all' ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span>🏠 All Models</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + allModels.length + '</span></div>';
|
|
314
451
|
for (const [provider, models] of providersToShow) {
|
|
315
|
-
html += '<div onclick="selectProvider(\\'' + provider + '\\')" class="px-3 py-2 rounded-
|
|
452
|
+
html += '<div onclick="selectProvider(\\'' + provider.replace(/'/g, "\\\\'") + '\\')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === provider ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span class="truncate">' + provider + '</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + models.length + '</span></div>';
|
|
316
453
|
}
|
|
317
454
|
container.innerHTML = html;
|
|
318
455
|
}
|
|
@@ -336,14 +473,14 @@ function generateHTML() {
|
|
|
336
473
|
container.innerHTML = '<div class="col-span-full text-center text-zinc-500 py-12">No models found</div>';
|
|
337
474
|
return;
|
|
338
475
|
}
|
|
339
|
-
container.innerHTML = filtered.map(m => '<div onclick="selectModel(\\'' + m.id + '\\')" class="p-
|
|
476
|
+
container.innerHTML = filtered.slice(0, 100).map(m => '<div onclick="selectModel(\\'' + m.id.replace(/'/g, "\\\\'") + '\\')" class="model-card p-5 rounded-2xl border cursor-pointer transition-all ' + (m.id === currentModelId ? 'bg-indigo-600/20 border-indigo-500 shadow-lg shadow-indigo-500/20' : 'bg-[#18181b] border-zinc-800 hover:border-zinc-700') + '"><div class="flex items-start justify-between mb-3"><h3 class="font-semibold text-white truncate text-base">' + m.name + '</h3>' + (m.id === currentModelId ? '<span class="text-xs bg-indigo-500 text-white px-3 py-1 rounded-full font-medium">Active</span>' : '') + '</div><p class="text-xs text-zinc-500 font-mono truncate mb-3">' + m.id + '</p>' + (m.cost ? '<div class="flex items-center gap-2 text-xs text-zinc-400"><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.input + '/M</span><span>→</span><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.output + '/M</span></div>' : '<div class="text-xs text-zinc-600">Free</div>') + '</div>').join('');
|
|
340
477
|
}
|
|
341
478
|
|
|
342
479
|
function filterData() {
|
|
343
480
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
344
481
|
if (searchTerm) {
|
|
345
482
|
const filtered = allModels.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
|
|
346
|
-
document.getElementById('sectionTitle').textContent = 'Search Results';
|
|
483
|
+
document.getElementById('sectionTitle').textContent = 'Search Results (' + filtered.length + ')';
|
|
347
484
|
renderModels(filtered);
|
|
348
485
|
renderProviders();
|
|
349
486
|
} else {
|
|
@@ -358,6 +495,7 @@ function generateHTML() {
|
|
|
358
495
|
currentModelId = modelId;
|
|
359
496
|
loadStatus();
|
|
360
497
|
renderModels(currentProvider === 'all' ? allModels : (groupedModels[currentProvider] || []));
|
|
498
|
+
document.getElementById('configJson').textContent = JSON.stringify({ env: { ANTHROPIC_BASE_URL: "http://localhost:8300/v1", ANTHROPIC_AUTH_TOKEN: "test", ANTHROPIC_MODEL: modelId } }, null, 2);
|
|
361
499
|
}
|
|
362
500
|
}
|
|
363
501
|
|
|
@@ -366,10 +504,15 @@ function generateHTML() {
|
|
|
366
504
|
const data = await res.json();
|
|
367
505
|
const model = allModels.find(m => m.id === data.currentModel);
|
|
368
506
|
document.getElementById('currentModelDisplay').textContent = model?.name || data.currentModel;
|
|
369
|
-
document.getElementById('
|
|
507
|
+
document.getElementById('healthDot').className = 'w-2.5 h-2.5 rounded-full animate-pulse ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
|
|
508
|
+
document.getElementById('connectionStatus').textContent = data.sessionId === 'active' ? 'Connected' : 'Disconnected';
|
|
370
509
|
document.getElementById('totalRequests').textContent = data.totalRequests.toLocaleString();
|
|
371
510
|
document.getElementById('totalTokens').textContent = data.totalTokensUsed.toLocaleString();
|
|
372
|
-
document.getElementById('
|
|
511
|
+
document.getElementById('sessionStatus').textContent = data.sessionId === 'active' ? 'Active' : 'Inactive';
|
|
512
|
+
document.getElementById('sessionStatus').className = 'text-lg font-semibold ' + (data.sessionId === 'active' ? 'text-green-400' : 'text-red-400');
|
|
513
|
+
document.getElementById('activeModelName').textContent = model?.name || data.currentModel;
|
|
514
|
+
document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models';
|
|
515
|
+
document.getElementById('totalProviders').textContent = Object.keys(groupedModels).length + ' providers';
|
|
373
516
|
currentModelId = data.currentModel;
|
|
374
517
|
}
|
|
375
518
|
|
|
@@ -383,6 +526,10 @@ function generateHTML() {
|
|
|
383
526
|
loadModels();
|
|
384
527
|
}
|
|
385
528
|
|
|
529
|
+
function updateStats() {
|
|
530
|
+
document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
|
|
531
|
+
}
|
|
532
|
+
|
|
386
533
|
loadModels();
|
|
387
534
|
loadStatus();
|
|
388
535
|
setInterval(loadStatus, 3000);
|