ocb-cli 1.0.1 → 1.0.3

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