ocb-cli 1.1.0 ā 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +92 -0
- package/dist/proxy.js +15 -235
- package/package.json +1 -1
- package/src/cli.ts +96 -0
- package/src/proxy.ts +15 -236
package/dist/cli.js
CHANGED
|
@@ -193,8 +193,81 @@ async function remove() {
|
|
|
193
193
|
await unconfigureClaudeCode();
|
|
194
194
|
log('ā
Bridge removed from Claude Code!', colors.green);
|
|
195
195
|
}
|
|
196
|
+
async function listModels() {
|
|
197
|
+
log('\nš Loading models...', colors.cyan);
|
|
198
|
+
try {
|
|
199
|
+
const res = await fetch('http://localhost:' + PROXY_PORT + '/api/models');
|
|
200
|
+
const data = await res.json();
|
|
201
|
+
const models = data.models || [];
|
|
202
|
+
log(`\nš Found ${models.length} models from ${data.providers?.length || 0} providers\n`, colors.green);
|
|
203
|
+
// Group by provider
|
|
204
|
+
const byProvider = {};
|
|
205
|
+
for (const m of models) {
|
|
206
|
+
if (!byProvider[m.provider])
|
|
207
|
+
byProvider[m.provider] = [];
|
|
208
|
+
byProvider[m.provider].push(m.name);
|
|
209
|
+
}
|
|
210
|
+
// Show first 5 providers with up to 3 models each
|
|
211
|
+
let count = 0;
|
|
212
|
+
for (const [provider, modelNames] of Object.entries(byProvider)) {
|
|
213
|
+
if (count >= 10)
|
|
214
|
+
break;
|
|
215
|
+
log(`\n${colors.cyan}${provider}${colors.reset}:`, colors.blue);
|
|
216
|
+
for (const name of modelNames.slice(0, 5)) {
|
|
217
|
+
log(` - ${name}`, colors.reset);
|
|
218
|
+
}
|
|
219
|
+
count++;
|
|
220
|
+
}
|
|
221
|
+
log('\n\nUse: ocb model <model-id>', colors.yellow);
|
|
222
|
+
log('Example: ocb model 302ai/claude-sonnet-4-5-20250929\n', colors.yellow);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
log('\nā Failed to fetch models. Is OCB running?', colors.yellow);
|
|
226
|
+
log(' Run: ocb start', colors.yellow);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function setModel(modelId) {
|
|
230
|
+
log(`\nš Switching to model: ${modelId}...`, colors.cyan);
|
|
231
|
+
try {
|
|
232
|
+
const res = await fetch('http://localhost:' + PROXY_PORT + '/api/model', {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: { 'Content-Type': 'application/json' },
|
|
235
|
+
body: JSON.stringify({ modelId })
|
|
236
|
+
});
|
|
237
|
+
const data = await res.json();
|
|
238
|
+
if (data.success) {
|
|
239
|
+
log('ā
Model switched! Restart Claude Code to use new model.', colors.green);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
log('ā ' + (data.error || 'Failed to switch model'), colors.yellow);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
log('\nā Failed to switch model. Is OCB running?', colors.yellow);
|
|
247
|
+
log(' Run: ocb start', colors.yellow);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function showStatus() {
|
|
251
|
+
try {
|
|
252
|
+
const res = await fetch('http://localhost:' + PROXY_PORT + '/api/status');
|
|
253
|
+
const data = await res.json();
|
|
254
|
+
log('\n' + '='.repeat(40), colors.blue);
|
|
255
|
+
log('š OCB Status', colors.blue);
|
|
256
|
+
log('='.repeat(40), colors.blue);
|
|
257
|
+
log(`Current Model: ${data.currentModel}`, colors.green);
|
|
258
|
+
log(`Total Requests: ${data.totalRequests}`, colors.reset);
|
|
259
|
+
log(`Total Tokens: ${data.totalTokensUsed}`, colors.reset);
|
|
260
|
+
log(`Session: ${data.sessionId || 'inactive'}`, colors.reset);
|
|
261
|
+
log('='.repeat(40) + '\n', colors.blue);
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
log('\nā OCB is not running', colors.yellow);
|
|
265
|
+
log(' Run: ocb start\n', colors.yellow);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
196
268
|
// CLI Commands
|
|
197
269
|
const command = process.argv[2];
|
|
270
|
+
const args = process.argv.slice(3);
|
|
198
271
|
switch (command) {
|
|
199
272
|
case 'setup':
|
|
200
273
|
setup();
|
|
@@ -211,6 +284,22 @@ switch (command) {
|
|
|
211
284
|
case 'install':
|
|
212
285
|
setup().then(() => start());
|
|
213
286
|
break;
|
|
287
|
+
case 'models':
|
|
288
|
+
listModels();
|
|
289
|
+
break;
|
|
290
|
+
case 'model':
|
|
291
|
+
if (args[0]) {
|
|
292
|
+
setModel(args[0]);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
log('\nā Please specify a model ID', colors.yellow);
|
|
296
|
+
log(' Usage: ocb model <model-id>', colors.yellow);
|
|
297
|
+
log(' List models: ocb models\n', colors.yellow);
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
case 'status':
|
|
301
|
+
showStatus();
|
|
302
|
+
break;
|
|
214
303
|
default:
|
|
215
304
|
log('\nš OCB - OpenCode Bridge CLI', colors.blue);
|
|
216
305
|
log('\nUsage:', colors.cyan);
|
|
@@ -218,6 +307,9 @@ switch (command) {
|
|
|
218
307
|
log(' ocb start - Start all services', colors.reset);
|
|
219
308
|
log(' ocb install - Setup + Start (all in one)', colors.reset);
|
|
220
309
|
log(' ocb stop - Stop all services', colors.reset);
|
|
310
|
+
log(' ocb status - Show current status', colors.reset);
|
|
311
|
+
log(' ocb models - List available models', colors.reset);
|
|
312
|
+
log(' ocb model <id> - Switch to a model', colors.reset);
|
|
221
313
|
log(' ocb remove - Remove configuration', colors.reset);
|
|
222
314
|
log('\nOr run with npx:', colors.yellow);
|
|
223
315
|
log(' npx ocb install\n', colors.yellow);
|
package/dist/proxy.js
CHANGED
|
@@ -130,8 +130,21 @@ app.use((req, res, next) => {
|
|
|
130
130
|
next();
|
|
131
131
|
});
|
|
132
132
|
app.get("/", (req, res) => {
|
|
133
|
-
res.
|
|
134
|
-
|
|
133
|
+
res.send(`OCB - OpenCode Bridge
|
|
134
|
+
========================
|
|
135
|
+
|
|
136
|
+
Status: Running
|
|
137
|
+
Model: ${currentModel}
|
|
138
|
+
Requests: ${totalRequests}
|
|
139
|
+
Tokens: ${totalTokensUsed}
|
|
140
|
+
|
|
141
|
+
CLI Commands:
|
|
142
|
+
ocb status - Show status
|
|
143
|
+
ocb models - List models
|
|
144
|
+
ocb model <id> - Switch model
|
|
145
|
+
|
|
146
|
+
Docs: https://github.com/veokhan/ocb
|
|
147
|
+
`);
|
|
135
148
|
});
|
|
136
149
|
app.get("/health", (req, res) => {
|
|
137
150
|
res.json({ status: "ok", timestamp: Date.now(), models: availableModels.length, providers: Object.keys(providers).length });
|
|
@@ -295,239 +308,6 @@ app.post("/v1/messages/count_tokens", (req, res) => {
|
|
|
295
308
|
res.json({ tokens: totalTokens });
|
|
296
309
|
});
|
|
297
310
|
const PORT = PROXY_PORT;
|
|
298
|
-
function generateHTML() {
|
|
299
|
-
return `<!DOCTYPE html>
|
|
300
|
-
<html lang="en" data-theme="dark" class="dark">
|
|
301
|
-
<head>
|
|
302
|
-
<meta charset="UTF-8">
|
|
303
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
304
|
-
<title>OCB - OpenCode Bridge</title>
|
|
305
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
306
|
-
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
307
|
-
<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">
|
|
308
|
-
<script>
|
|
309
|
-
tailwind.config = {
|
|
310
|
-
theme: {
|
|
311
|
-
extend: {
|
|
312
|
-
colors: {
|
|
313
|
-
space: { 950: '#0a0a0f', 900: '#121218', 800: '#1a1a24', 700: '#24242e', border: '#2a2a38' },
|
|
314
|
-
neon: { purple: '#a855f7', cyan: '#22d3ee', green: '#22c55e', pink: '#ec4899' }
|
|
315
|
-
},
|
|
316
|
-
fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] }
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
</script>
|
|
321
|
-
<style>
|
|
322
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
323
|
-
body { font-family: 'Inter', -apple-system, sans-serif; background: #0a0a0f; color: #e4e4e7; min-height: 100vh; }
|
|
324
|
-
[x-cloak] { display: none !important; }
|
|
325
|
-
::-webkit-scrollbar { width: 6px; }
|
|
326
|
-
::-webkit-scrollbar-track { background: transparent; }
|
|
327
|
-
::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
|
|
328
|
-
.nav-item { transition: all 0.2s; }
|
|
329
|
-
.nav-item.active { background: linear-gradient(90deg, rgba(168,85,247,0.2) 0%, transparent 100%); border-left: 2px solid #a855f7; color: white; }
|
|
330
|
-
</style>
|
|
331
|
-
</head>
|
|
332
|
-
<body class="overflow-hidden" x-data="app()" x-init="init()">
|
|
333
|
-
<div class="h-14 border-b border-space-border flex items-center px-4 justify-between bg-space-900/80 backdrop-blur-md">
|
|
334
|
-
<div class="flex items-center gap-3">
|
|
335
|
-
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-neon-purple to-blue-600 flex items-center justify-center text-white font-bold shadow-lg shadow-neon-purple/20">OC</div>
|
|
336
|
-
<div class="flex flex-col">
|
|
337
|
-
<span class="text-sm font-bold tracking-wide text-white">OCB</span>
|
|
338
|
-
<span class="text-[10px] text-gray-500 font-mono">OPENCODE BRIDGE</span>
|
|
339
|
-
</div>
|
|
340
|
-
</div>
|
|
341
|
-
<div class="flex items-center gap-4">
|
|
342
|
-
<div class="flex items-center gap-2 px-3 py-1 rounded-full text-xs font-mono border transition-all" :class="connected ? 'bg-neon-green/10 border-neon-green/20 text-neon-green' : 'bg-red-500/10 border-red-500/20 text-red-400'">
|
|
343
|
-
<div class="w-1.5 h-1.5 rounded-full" :class="connected ? 'bg-neon-green shadow-lg shadow-neon-green/50' : 'bg-red-400'"></div>
|
|
344
|
-
<span x-text="connected ? 'Connected' : 'Disconnected'"></span>
|
|
345
|
-
</div>
|
|
346
|
-
<button @click="refresh()" class="p-2 hover:bg-white/5 rounded-lg transition text-gray-400">
|
|
347
|
-
<svg class="w-4 h-4" :class="{'animate-spin': loading}" 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>
|
|
348
|
-
</button>
|
|
349
|
-
</div>
|
|
350
|
-
</div>
|
|
351
|
-
<div class="flex h-[calc(100vh-56px)]">
|
|
352
|
-
<nav class="w-56 bg-space-900 border-r border-space-border flex flex-col">
|
|
353
|
-
<div class="p-4 space-y-1">
|
|
354
|
-
<button @click="tab='dashboard'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='dashboard'?'active':''">
|
|
355
|
-
<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 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
|
|
356
|
-
Dashboard
|
|
357
|
-
</button>
|
|
358
|
-
<button @click="tab='models'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='models'?'active':''">
|
|
359
|
-
<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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>
|
|
360
|
-
Models
|
|
361
|
-
</button>
|
|
362
|
-
<button @click="tab='settings'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='settings'?'active':''">
|
|
363
|
-
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
|
364
|
-
Settings
|
|
365
|
-
</button>
|
|
366
|
-
</div>
|
|
367
|
-
<div class="mt-auto p-4 border-t border-space-border">
|
|
368
|
-
<div class="text-xs text-gray-500 font-mono">
|
|
369
|
-
<div class="flex justify-between mb-1"><span>Port:</span><span class="text-gray-400">8300</span></div>
|
|
370
|
-
<div class="flex justify-between"><span>OpenCode:</span><span class="text-neon-green">localhost:4096</span></div>
|
|
371
|
-
</div>
|
|
372
|
-
</div>
|
|
373
|
-
</nav>
|
|
374
|
-
<main class="flex-1 overflow-auto bg-space-950 p-6">
|
|
375
|
-
<div x-show="tab==='dashboard'" x-transition>
|
|
376
|
-
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
377
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
378
|
-
<div class="flex items-center gap-3 mb-3">
|
|
379
|
-
<div class="w-10 h-10 bg-neon-purple/20 rounded-xl flex items-center justify-center text-neon-purple">š</div>
|
|
380
|
-
<div><p class="text-xs text-gray-500">Total Requests</p><p class="text-2xl font-bold text-white" x-text="stats.requests.toLocaleString()">0</p></div>
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
383
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
384
|
-
<div class="flex items-center gap-3 mb-3">
|
|
385
|
-
<div class="w-10 h-10 bg-neon-cyan/20 rounded-xl flex items-center justify-center text-neon-cyan">šÆ</div>
|
|
386
|
-
<div><p class="text-xs text-gray-500">Total Tokens</p><p class="text-2xl font-bold text-white" x-text="stats.tokens.toLocaleString()">0</p></div>
|
|
387
|
-
</div>
|
|
388
|
-
</div>
|
|
389
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
390
|
-
<div class="flex items-center gap-3 mb-3">
|
|
391
|
-
<div class="w-10 h-10 bg-neon-green/20 rounded-xl flex items-center justify-center text-neon-green">š¤</div>
|
|
392
|
-
<div><p class="text-xs text-gray-500">Active Model</p><p class="text-lg font-semibold text-white truncate" x-text="currentModelName">-</p></div>
|
|
393
|
-
</div>
|
|
394
|
-
</div>
|
|
395
|
-
</div>
|
|
396
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
397
|
-
<h3 class="text-lg font-semibold text-white mb-4">Current Session</h3>
|
|
398
|
-
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
399
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Session ID</span><span class="text-gray-300 font-mono text-xs" x-text="stats.sessionId || 'inactive'">-</span></div>
|
|
400
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Status</span><span class="text-neon-green" x-text="stats.sessionId ? 'Active' : 'Inactive'">-</span></div>
|
|
401
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Models Available</span><span class="text-gray-300" x-text="stats.models">0</span></div>
|
|
402
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Providers</span><span class="text-gray-300" x-text="stats.providers">0</span></div>
|
|
403
|
-
</div>
|
|
404
|
-
<div class="mt-4 flex gap-3">
|
|
405
|
-
<button @click="resetSession()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Reset Session</button>
|
|
406
|
-
<button @click="refreshModels()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Refresh Models</button>
|
|
407
|
-
</div>
|
|
408
|
-
</div>
|
|
409
|
-
</div>
|
|
410
|
-
<div x-show="tab==='models'" x-transition style="display:none;">
|
|
411
|
-
<div class="mb-4 flex gap-4">
|
|
412
|
-
<input type="text" x-model="search" @input="filterModels()" placeholder="Search models..." class="flex-1 px-4 py-2.5 bg-space-900 border border-space-border rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-neon-purple">
|
|
413
|
-
<select x-model="selectedProvider" @change="filterModels()" class="px-4 py-2.5 bg-space-900 border border-space-border rounded-xl text-white focus:outline-none">
|
|
414
|
-
<option value="all">All Providers</option>
|
|
415
|
-
<template x-for="p in providers" :key="p"><option :value="p" x-text="p"></option></template>
|
|
416
|
-
</select>
|
|
417
|
-
</div>
|
|
418
|
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 max-h-[calc(100vh-220px)] overflow-y-auto">
|
|
419
|
-
<template x-for="m in filteredModels" :key="m.id">
|
|
420
|
-
<div @click="selectModel(m.id)" class="p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02]" :class="m.id===currentModel?'bg-neon-purple/20 border-neon-purple':'bg-space-900 border-space-border hover:border-space-700'">
|
|
421
|
-
<div class="flex items-start justify-between mb-2">
|
|
422
|
-
<h3 class="font-semibold text-white truncate" x-text="m.name"></h3>
|
|
423
|
-
<span x-show="m.id===currentModel" class="text-xs bg-neon-purple text-white px-2 py-0.5 rounded">Active</span>
|
|
424
|
-
</div>
|
|
425
|
-
<p class="text-xs text-gray-500 font-mono truncate mb-2" x-text="m.id"></p>
|
|
426
|
-
<p class="text-xs text-gray-600" x-text="m.provider"></p>
|
|
427
|
-
</div>
|
|
428
|
-
</template>
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
<div x-show="tab==='settings'" x-transition style="display:none;">
|
|
432
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-6 mb-4">
|
|
433
|
-
<h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
|
|
434
|
-
<p class="text-gray-400 text-sm mb-4">Configure Claude Code to use OCB as the API endpoint:</p>
|
|
435
|
-
<div class="bg-space-950 rounded-xl p-4 font-mono text-sm text-gray-300 overflow-x-auto mb-4">
|
|
436
|
-
<pre x-text="configJson"></pre>
|
|
437
|
-
</div>
|
|
438
|
-
<button @click="applyToClaude()" class="px-6 py-2.5 bg-neon-purple hover:bg-neon-purple/80 rounded-xl text-white font-medium transition flex items-center gap-2">
|
|
439
|
-
<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="M5 13l4 4L19 7"></path></svg>
|
|
440
|
-
Apply to Claude Code
|
|
441
|
-
</button>
|
|
442
|
-
</div>
|
|
443
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-6">
|
|
444
|
-
<h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
|
|
445
|
-
<div class="space-y-2 font-mono text-sm">
|
|
446
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Health</span><span class="text-neon-cyan">/health</span></div>
|
|
447
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Status</span><span class="text-neon-cyan">/api/status</span></div>
|
|
448
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Models</span><span class="text-neon-cyan">/v1/models</span></div>
|
|
449
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Messages</span><span class="text-neon-cyan">/v1/messages</span></div>
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
|
-
</div>
|
|
453
|
-
</main>
|
|
454
|
-
</div>
|
|
455
|
-
<script>
|
|
456
|
-
function app() {
|
|
457
|
-
return {
|
|
458
|
-
tab: 'dashboard',
|
|
459
|
-
loading: false,
|
|
460
|
-
connected: false,
|
|
461
|
-
search: '',
|
|
462
|
-
selectedProvider: 'all',
|
|
463
|
-
models: [],
|
|
464
|
-
providers: [],
|
|
465
|
-
filteredModels: [],
|
|
466
|
-
currentModel: '',
|
|
467
|
-
currentModelName: '',
|
|
468
|
-
stats: { requests: 0, tokens: 0, sessionId: '', models: 0, providers: 0 },
|
|
469
|
-
configJson: '',
|
|
470
|
-
async init() {
|
|
471
|
-
await this.refresh();
|
|
472
|
-
setInterval(() => this.refresh(), 3000);
|
|
473
|
-
},
|
|
474
|
-
async refresh() {
|
|
475
|
-
this.loading = true;
|
|
476
|
-
try {
|
|
477
|
-
const [statusRes, modelsRes] = await Promise.all([fetch('/api/status'), fetch('/api/models')]);
|
|
478
|
-
const status = await statusRes.json();
|
|
479
|
-
const modelsData = await modelsRes.json();
|
|
480
|
-
this.connected = status.sessionId === 'active';
|
|
481
|
-
this.stats = { requests: status.totalRequests, tokens: status.totalTokensUsed, sessionId: status.sessionId, models: modelsData.models?.length || 0, providers: modelsData.providers?.length || 0 };
|
|
482
|
-
this.currentModel = status.currentModel;
|
|
483
|
-
this.models = modelsData.models || [];
|
|
484
|
-
this.providers = [...new Set(this.models.map(m => m.provider))].sort();
|
|
485
|
-
const current = this.models.find(m => m.id === this.currentModel);
|
|
486
|
-
this.currentModelName = current?.name || this.currentModel;
|
|
487
|
-
this.filterModels();
|
|
488
|
-
this.updateConfig();
|
|
489
|
-
} catch (e) { console.error(e); }
|
|
490
|
-
this.loading = false;
|
|
491
|
-
},
|
|
492
|
-
filterModels() {
|
|
493
|
-
let filtered = this.models;
|
|
494
|
-
if (this.selectedProvider !== 'all') filtered = filtered.filter(m => m.provider === this.selectedProvider);
|
|
495
|
-
if (this.search) {
|
|
496
|
-
const s = this.search.toLowerCase();
|
|
497
|
-
filtered = filtered.filter(m => m.name.toLowerCase().includes(s) || m.id.toLowerCase().includes(s));
|
|
498
|
-
}
|
|
499
|
-
this.filteredModels = filtered.slice(0, 100);
|
|
500
|
-
},
|
|
501
|
-
async selectModel(modelId) {
|
|
502
|
-
await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
|
|
503
|
-
this.currentModel = modelId;
|
|
504
|
-
this.refresh();
|
|
505
|
-
},
|
|
506
|
-
async resetSession() {
|
|
507
|
-
await fetch('/api/reset-session', { method: 'POST' });
|
|
508
|
-
this.refresh();
|
|
509
|
-
},
|
|
510
|
-
async refreshModels() {
|
|
511
|
-
await fetch('/api/refresh-models', { method: 'POST' });
|
|
512
|
-
this.refresh();
|
|
513
|
-
},
|
|
514
|
-
updateConfig() {
|
|
515
|
-
this.configJson = JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'http://localhost:8300', ANTHROPIC_API_KEY: 'test', ANTHROPIC_MODEL: this.currentModel } }, null, 2);
|
|
516
|
-
},
|
|
517
|
-
async applyToClaude() {
|
|
518
|
-
try {
|
|
519
|
-
const response = await fetch('/api/apply-claude-config', { method: 'POST' });
|
|
520
|
-
const data = await response.json();
|
|
521
|
-
alert(data.success ? 'Applied to Claude Code! Restart Claude Code to use.' : 'Failed: ' + data.error);
|
|
522
|
-
} catch (e) { alert('Error: ' + e.message); }
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
</script>
|
|
527
|
-
</body>
|
|
528
|
-
</html>`;
|
|
529
|
-
}
|
|
530
311
|
app.listen(PORT, () => {
|
|
531
312
|
console.error(`OpenCode Bridge running on http://localhost:${PORT}`);
|
|
532
|
-
console.error(`Dashboard: http://localhost:${PORT}`);
|
|
533
313
|
});
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -241,8 +241,86 @@ async function remove() {
|
|
|
241
241
|
log('ā
Bridge removed from Claude Code!', colors.green);
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
async function listModels() {
|
|
245
|
+
log('\nš Loading models...', colors.cyan);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const res = await fetch('http://localhost:' + PROXY_PORT + '/api/models');
|
|
249
|
+
const data = await res.json();
|
|
250
|
+
const models = data.models || [];
|
|
251
|
+
|
|
252
|
+
log(`\nš Found ${models.length} models from ${data.providers?.length || 0} providers\n`, colors.green);
|
|
253
|
+
|
|
254
|
+
// Group by provider
|
|
255
|
+
const byProvider: Record<string, string[]> = {};
|
|
256
|
+
for (const m of models) {
|
|
257
|
+
if (!byProvider[m.provider]) byProvider[m.provider] = [];
|
|
258
|
+
byProvider[m.provider].push(m.name);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Show first 5 providers with up to 3 models each
|
|
262
|
+
let count = 0;
|
|
263
|
+
for (const [provider, modelNames] of Object.entries(byProvider)) {
|
|
264
|
+
if (count >= 10) break;
|
|
265
|
+
log(`\n${colors.cyan}${provider}${colors.reset}:`, colors.blue);
|
|
266
|
+
for (const name of modelNames.slice(0, 5)) {
|
|
267
|
+
log(` - ${name}`, colors.reset);
|
|
268
|
+
}
|
|
269
|
+
count++;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
log('\n\nUse: ocb model <model-id>', colors.yellow);
|
|
273
|
+
log('Example: ocb model 302ai/claude-sonnet-4-5-20250929\n', colors.yellow);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
log('\nā Failed to fetch models. Is OCB running?', colors.yellow);
|
|
276
|
+
log(' Run: ocb start', colors.yellow);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function setModel(modelId: string) {
|
|
281
|
+
log(`\nš Switching to model: ${modelId}...`, colors.cyan);
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const res = await fetch('http://localhost:' + PROXY_PORT + '/api/model', {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
headers: { 'Content-Type': 'application/json' },
|
|
287
|
+
body: JSON.stringify({ modelId })
|
|
288
|
+
});
|
|
289
|
+
const data = await res.json();
|
|
290
|
+
|
|
291
|
+
if (data.success) {
|
|
292
|
+
log('ā
Model switched! Restart Claude Code to use new model.', colors.green);
|
|
293
|
+
} else {
|
|
294
|
+
log('ā ' + (data.error || 'Failed to switch model'), colors.yellow);
|
|
295
|
+
}
|
|
296
|
+
} catch (e) {
|
|
297
|
+
log('\nā Failed to switch model. Is OCB running?', colors.yellow);
|
|
298
|
+
log(' Run: ocb start', colors.yellow);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function showStatus() {
|
|
303
|
+
try {
|
|
304
|
+
const res = await fetch('http://localhost:' + PROXY_PORT + '/api/status');
|
|
305
|
+
const data = await res.json();
|
|
306
|
+
|
|
307
|
+
log('\n' + '='.repeat(40), colors.blue);
|
|
308
|
+
log('š OCB Status', colors.blue);
|
|
309
|
+
log('='.repeat(40), colors.blue);
|
|
310
|
+
log(`Current Model: ${data.currentModel}`, colors.green);
|
|
311
|
+
log(`Total Requests: ${data.totalRequests}`, colors.reset);
|
|
312
|
+
log(`Total Tokens: ${data.totalTokensUsed}`, colors.reset);
|
|
313
|
+
log(`Session: ${data.sessionId || 'inactive'}`, colors.reset);
|
|
314
|
+
log('='.repeat(40) + '\n', colors.blue);
|
|
315
|
+
} catch (e) {
|
|
316
|
+
log('\nā OCB is not running', colors.yellow);
|
|
317
|
+
log(' Run: ocb start\n', colors.yellow);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
244
321
|
// CLI Commands
|
|
245
322
|
const command = process.argv[2];
|
|
323
|
+
const args = process.argv.slice(3);
|
|
246
324
|
|
|
247
325
|
switch (command) {
|
|
248
326
|
case 'setup':
|
|
@@ -260,6 +338,21 @@ switch (command) {
|
|
|
260
338
|
case 'install':
|
|
261
339
|
setup().then(() => start());
|
|
262
340
|
break;
|
|
341
|
+
case 'models':
|
|
342
|
+
listModels();
|
|
343
|
+
break;
|
|
344
|
+
case 'model':
|
|
345
|
+
if (args[0]) {
|
|
346
|
+
setModel(args[0]);
|
|
347
|
+
} else {
|
|
348
|
+
log('\nā Please specify a model ID', colors.yellow);
|
|
349
|
+
log(' Usage: ocb model <model-id>', colors.yellow);
|
|
350
|
+
log(' List models: ocb models\n', colors.yellow);
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
case 'status':
|
|
354
|
+
showStatus();
|
|
355
|
+
break;
|
|
263
356
|
default:
|
|
264
357
|
log('\nš OCB - OpenCode Bridge CLI', colors.blue);
|
|
265
358
|
log('\nUsage:', colors.cyan);
|
|
@@ -267,6 +360,9 @@ switch (command) {
|
|
|
267
360
|
log(' ocb start - Start all services', colors.reset);
|
|
268
361
|
log(' ocb install - Setup + Start (all in one)', colors.reset);
|
|
269
362
|
log(' ocb stop - Stop all services', colors.reset);
|
|
363
|
+
log(' ocb status - Show current status', colors.reset);
|
|
364
|
+
log(' ocb models - List available models', colors.reset);
|
|
365
|
+
log(' ocb model <id> - Switch to a model', colors.reset);
|
|
270
366
|
log(' ocb remove - Remove configuration', colors.reset);
|
|
271
367
|
log('\nOr run with npx:', colors.yellow);
|
|
272
368
|
log(' npx ocb install\n', colors.yellow);
|
package/src/proxy.ts
CHANGED
|
@@ -159,8 +159,21 @@ app.use((req, res, next) => {
|
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
app.get("/", (req, res) => {
|
|
162
|
-
res.
|
|
163
|
-
|
|
162
|
+
res.send(`OCB - OpenCode Bridge
|
|
163
|
+
========================
|
|
164
|
+
|
|
165
|
+
Status: Running
|
|
166
|
+
Model: ${currentModel}
|
|
167
|
+
Requests: ${totalRequests}
|
|
168
|
+
Tokens: ${totalTokensUsed}
|
|
169
|
+
|
|
170
|
+
CLI Commands:
|
|
171
|
+
ocb status - Show status
|
|
172
|
+
ocb models - List models
|
|
173
|
+
ocb model <id> - Switch model
|
|
174
|
+
|
|
175
|
+
Docs: https://github.com/veokhan/ocb
|
|
176
|
+
`);
|
|
164
177
|
});
|
|
165
178
|
|
|
166
179
|
app.get("/health", (req, res) => {
|
|
@@ -333,240 +346,6 @@ app.post("/v1/messages/count_tokens", (req, res) => {
|
|
|
333
346
|
|
|
334
347
|
const PORT = PROXY_PORT;
|
|
335
348
|
|
|
336
|
-
function generateHTML() {
|
|
337
|
-
return `<!DOCTYPE html>
|
|
338
|
-
<html lang="en" data-theme="dark" class="dark">
|
|
339
|
-
<head>
|
|
340
|
-
<meta charset="UTF-8">
|
|
341
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
342
|
-
<title>OCB - OpenCode Bridge</title>
|
|
343
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
344
|
-
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
345
|
-
<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">
|
|
346
|
-
<script>
|
|
347
|
-
tailwind.config = {
|
|
348
|
-
theme: {
|
|
349
|
-
extend: {
|
|
350
|
-
colors: {
|
|
351
|
-
space: { 950: '#0a0a0f', 900: '#121218', 800: '#1a1a24', 700: '#24242e', border: '#2a2a38' },
|
|
352
|
-
neon: { purple: '#a855f7', cyan: '#22d3ee', green: '#22c55e', pink: '#ec4899' }
|
|
353
|
-
},
|
|
354
|
-
fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] }
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
</script>
|
|
359
|
-
<style>
|
|
360
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
361
|
-
body { font-family: 'Inter', -apple-system, sans-serif; background: #0a0a0f; color: #e4e4e7; min-height: 100vh; }
|
|
362
|
-
[x-cloak] { display: none !important; }
|
|
363
|
-
::-webkit-scrollbar { width: 6px; }
|
|
364
|
-
::-webkit-scrollbar-track { background: transparent; }
|
|
365
|
-
::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
|
|
366
|
-
.nav-item { transition: all 0.2s; }
|
|
367
|
-
.nav-item.active { background: linear-gradient(90deg, rgba(168,85,247,0.2) 0%, transparent 100%); border-left: 2px solid #a855f7; color: white; }
|
|
368
|
-
</style>
|
|
369
|
-
</head>
|
|
370
|
-
<body class="overflow-hidden" x-data="app()" x-init="init()">
|
|
371
|
-
<div class="h-14 border-b border-space-border flex items-center px-4 justify-between bg-space-900/80 backdrop-blur-md">
|
|
372
|
-
<div class="flex items-center gap-3">
|
|
373
|
-
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-neon-purple to-blue-600 flex items-center justify-center text-white font-bold shadow-lg shadow-neon-purple/20">OC</div>
|
|
374
|
-
<div class="flex flex-col">
|
|
375
|
-
<span class="text-sm font-bold tracking-wide text-white">OCB</span>
|
|
376
|
-
<span class="text-[10px] text-gray-500 font-mono">OPENCODE BRIDGE</span>
|
|
377
|
-
</div>
|
|
378
|
-
</div>
|
|
379
|
-
<div class="flex items-center gap-4">
|
|
380
|
-
<div class="flex items-center gap-2 px-3 py-1 rounded-full text-xs font-mono border transition-all" :class="connected ? 'bg-neon-green/10 border-neon-green/20 text-neon-green' : 'bg-red-500/10 border-red-500/20 text-red-400'">
|
|
381
|
-
<div class="w-1.5 h-1.5 rounded-full" :class="connected ? 'bg-neon-green shadow-lg shadow-neon-green/50' : 'bg-red-400'"></div>
|
|
382
|
-
<span x-text="connected ? 'Connected' : 'Disconnected'"></span>
|
|
383
|
-
</div>
|
|
384
|
-
<button @click="refresh()" class="p-2 hover:bg-white/5 rounded-lg transition text-gray-400">
|
|
385
|
-
<svg class="w-4 h-4" :class="{'animate-spin': loading}" 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>
|
|
386
|
-
</button>
|
|
387
|
-
</div>
|
|
388
|
-
</div>
|
|
389
|
-
<div class="flex h-[calc(100vh-56px)]">
|
|
390
|
-
<nav class="w-56 bg-space-900 border-r border-space-border flex flex-col">
|
|
391
|
-
<div class="p-4 space-y-1">
|
|
392
|
-
<button @click="tab='dashboard'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='dashboard'?'active':''">
|
|
393
|
-
<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 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
|
|
394
|
-
Dashboard
|
|
395
|
-
</button>
|
|
396
|
-
<button @click="tab='models'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='models'?'active':''">
|
|
397
|
-
<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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>
|
|
398
|
-
Models
|
|
399
|
-
</button>
|
|
400
|
-
<button @click="tab='settings'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='settings'?'active':''">
|
|
401
|
-
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
|
402
|
-
Settings
|
|
403
|
-
</button>
|
|
404
|
-
</div>
|
|
405
|
-
<div class="mt-auto p-4 border-t border-space-border">
|
|
406
|
-
<div class="text-xs text-gray-500 font-mono">
|
|
407
|
-
<div class="flex justify-between mb-1"><span>Port:</span><span class="text-gray-400">8300</span></div>
|
|
408
|
-
<div class="flex justify-between"><span>OpenCode:</span><span class="text-neon-green">localhost:4096</span></div>
|
|
409
|
-
</div>
|
|
410
|
-
</div>
|
|
411
|
-
</nav>
|
|
412
|
-
<main class="flex-1 overflow-auto bg-space-950 p-6">
|
|
413
|
-
<div x-show="tab==='dashboard'" x-transition>
|
|
414
|
-
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
415
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
416
|
-
<div class="flex items-center gap-3 mb-3">
|
|
417
|
-
<div class="w-10 h-10 bg-neon-purple/20 rounded-xl flex items-center justify-center text-neon-purple">š</div>
|
|
418
|
-
<div><p class="text-xs text-gray-500">Total Requests</p><p class="text-2xl font-bold text-white" x-text="stats.requests.toLocaleString()">0</p></div>
|
|
419
|
-
</div>
|
|
420
|
-
</div>
|
|
421
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
422
|
-
<div class="flex items-center gap-3 mb-3">
|
|
423
|
-
<div class="w-10 h-10 bg-neon-cyan/20 rounded-xl flex items-center justify-center text-neon-cyan">šÆ</div>
|
|
424
|
-
<div><p class="text-xs text-gray-500">Total Tokens</p><p class="text-2xl font-bold text-white" x-text="stats.tokens.toLocaleString()">0</p></div>
|
|
425
|
-
</div>
|
|
426
|
-
</div>
|
|
427
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
428
|
-
<div class="flex items-center gap-3 mb-3">
|
|
429
|
-
<div class="w-10 h-10 bg-neon-green/20 rounded-xl flex items-center justify-center text-neon-green">š¤</div>
|
|
430
|
-
<div><p class="text-xs text-gray-500">Active Model</p><p class="text-lg font-semibold text-white truncate" x-text="currentModelName">-</p></div>
|
|
431
|
-
</div>
|
|
432
|
-
</div>
|
|
433
|
-
</div>
|
|
434
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
435
|
-
<h3 class="text-lg font-semibold text-white mb-4">Current Session</h3>
|
|
436
|
-
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
437
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Session ID</span><span class="text-gray-300 font-mono text-xs" x-text="stats.sessionId || 'inactive'">-</span></div>
|
|
438
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Status</span><span class="text-neon-green" x-text="stats.sessionId ? 'Active' : 'Inactive'">-</span></div>
|
|
439
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Models Available</span><span class="text-gray-300" x-text="stats.models">0</span></div>
|
|
440
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Providers</span><span class="text-gray-300" x-text="stats.providers">0</span></div>
|
|
441
|
-
</div>
|
|
442
|
-
<div class="mt-4 flex gap-3">
|
|
443
|
-
<button @click="resetSession()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Reset Session</button>
|
|
444
|
-
<button @click="refreshModels()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Refresh Models</button>
|
|
445
|
-
</div>
|
|
446
|
-
</div>
|
|
447
|
-
</div>
|
|
448
|
-
<div x-show="tab==='models'" x-transition style="display:none;">
|
|
449
|
-
<div class="mb-4 flex gap-4">
|
|
450
|
-
<input type="text" x-model="search" @input="filterModels()" placeholder="Search models..." class="flex-1 px-4 py-2.5 bg-space-900 border border-space-border rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-neon-purple">
|
|
451
|
-
<select x-model="selectedProvider" @change="filterModels()" class="px-4 py-2.5 bg-space-900 border border-space-border rounded-xl text-white focus:outline-none">
|
|
452
|
-
<option value="all">All Providers</option>
|
|
453
|
-
<template x-for="p in providers" :key="p"><option :value="p" x-text="p"></option></template>
|
|
454
|
-
</select>
|
|
455
|
-
</div>
|
|
456
|
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 max-h-[calc(100vh-220px)] overflow-y-auto">
|
|
457
|
-
<template x-for="m in filteredModels" :key="m.id">
|
|
458
|
-
<div @click="selectModel(m.id)" class="p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02]" :class="m.id===currentModel?'bg-neon-purple/20 border-neon-purple':'bg-space-900 border-space-border hover:border-space-700'">
|
|
459
|
-
<div class="flex items-start justify-between mb-2">
|
|
460
|
-
<h3 class="font-semibold text-white truncate" x-text="m.name"></h3>
|
|
461
|
-
<span x-show="m.id===currentModel" class="text-xs bg-neon-purple text-white px-2 py-0.5 rounded">Active</span>
|
|
462
|
-
</div>
|
|
463
|
-
<p class="text-xs text-gray-500 font-mono truncate mb-2" x-text="m.id"></p>
|
|
464
|
-
<p class="text-xs text-gray-600" x-text="m.provider"></p>
|
|
465
|
-
</div>
|
|
466
|
-
</template>
|
|
467
|
-
</div>
|
|
468
|
-
</div>
|
|
469
|
-
<div x-show="tab==='settings'" x-transition style="display:none;">
|
|
470
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-6 mb-4">
|
|
471
|
-
<h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
|
|
472
|
-
<p class="text-gray-400 text-sm mb-4">Configure Claude Code to use OCB as the API endpoint:</p>
|
|
473
|
-
<div class="bg-space-950 rounded-xl p-4 font-mono text-sm text-gray-300 overflow-x-auto mb-4">
|
|
474
|
-
<pre x-text="configJson"></pre>
|
|
475
|
-
</div>
|
|
476
|
-
<button @click="applyToClaude()" class="px-6 py-2.5 bg-neon-purple hover:bg-neon-purple/80 rounded-xl text-white font-medium transition flex items-center gap-2">
|
|
477
|
-
<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="M5 13l4 4L19 7"></path></svg>
|
|
478
|
-
Apply to Claude Code
|
|
479
|
-
</button>
|
|
480
|
-
</div>
|
|
481
|
-
<div class="bg-space-900 rounded-xl border border-space-border p-6">
|
|
482
|
-
<h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
|
|
483
|
-
<div class="space-y-2 font-mono text-sm">
|
|
484
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Health</span><span class="text-neon-cyan">/health</span></div>
|
|
485
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Status</span><span class="text-neon-cyan">/api/status</span></div>
|
|
486
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Models</span><span class="text-neon-cyan">/v1/models</span></div>
|
|
487
|
-
<div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Messages</span><span class="text-neon-cyan">/v1/messages</span></div>
|
|
488
|
-
</div>
|
|
489
|
-
</div>
|
|
490
|
-
</div>
|
|
491
|
-
</main>
|
|
492
|
-
</div>
|
|
493
|
-
<script>
|
|
494
|
-
function app() {
|
|
495
|
-
return {
|
|
496
|
-
tab: 'dashboard',
|
|
497
|
-
loading: false,
|
|
498
|
-
connected: false,
|
|
499
|
-
search: '',
|
|
500
|
-
selectedProvider: 'all',
|
|
501
|
-
models: [],
|
|
502
|
-
providers: [],
|
|
503
|
-
filteredModels: [],
|
|
504
|
-
currentModel: '',
|
|
505
|
-
currentModelName: '',
|
|
506
|
-
stats: { requests: 0, tokens: 0, sessionId: '', models: 0, providers: 0 },
|
|
507
|
-
configJson: '',
|
|
508
|
-
async init() {
|
|
509
|
-
await this.refresh();
|
|
510
|
-
setInterval(() => this.refresh(), 3000);
|
|
511
|
-
},
|
|
512
|
-
async refresh() {
|
|
513
|
-
this.loading = true;
|
|
514
|
-
try {
|
|
515
|
-
const [statusRes, modelsRes] = await Promise.all([fetch('/api/status'), fetch('/api/models')]);
|
|
516
|
-
const status = await statusRes.json();
|
|
517
|
-
const modelsData = await modelsRes.json();
|
|
518
|
-
this.connected = status.sessionId === 'active';
|
|
519
|
-
this.stats = { requests: status.totalRequests, tokens: status.totalTokensUsed, sessionId: status.sessionId, models: modelsData.models?.length || 0, providers: modelsData.providers?.length || 0 };
|
|
520
|
-
this.currentModel = status.currentModel;
|
|
521
|
-
this.models = modelsData.models || [];
|
|
522
|
-
this.providers = [...new Set(this.models.map(m => m.provider))].sort();
|
|
523
|
-
const current = this.models.find(m => m.id === this.currentModel);
|
|
524
|
-
this.currentModelName = current?.name || this.currentModel;
|
|
525
|
-
this.filterModels();
|
|
526
|
-
this.updateConfig();
|
|
527
|
-
} catch (e) { console.error(e); }
|
|
528
|
-
this.loading = false;
|
|
529
|
-
},
|
|
530
|
-
filterModels() {
|
|
531
|
-
let filtered = this.models;
|
|
532
|
-
if (this.selectedProvider !== 'all') filtered = filtered.filter(m => m.provider === this.selectedProvider);
|
|
533
|
-
if (this.search) {
|
|
534
|
-
const s = this.search.toLowerCase();
|
|
535
|
-
filtered = filtered.filter(m => m.name.toLowerCase().includes(s) || m.id.toLowerCase().includes(s));
|
|
536
|
-
}
|
|
537
|
-
this.filteredModels = filtered.slice(0, 100);
|
|
538
|
-
},
|
|
539
|
-
async selectModel(modelId) {
|
|
540
|
-
await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
|
|
541
|
-
this.currentModel = modelId;
|
|
542
|
-
this.refresh();
|
|
543
|
-
},
|
|
544
|
-
async resetSession() {
|
|
545
|
-
await fetch('/api/reset-session', { method: 'POST' });
|
|
546
|
-
this.refresh();
|
|
547
|
-
},
|
|
548
|
-
async refreshModels() {
|
|
549
|
-
await fetch('/api/refresh-models', { method: 'POST' });
|
|
550
|
-
this.refresh();
|
|
551
|
-
},
|
|
552
|
-
updateConfig() {
|
|
553
|
-
this.configJson = JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'http://localhost:8300', ANTHROPIC_API_KEY: 'test', ANTHROPIC_MODEL: this.currentModel } }, null, 2);
|
|
554
|
-
},
|
|
555
|
-
async applyToClaude() {
|
|
556
|
-
try {
|
|
557
|
-
const response = await fetch('/api/apply-claude-config', { method: 'POST' });
|
|
558
|
-
const data = await response.json();
|
|
559
|
-
alert(data.success ? 'Applied to Claude Code! Restart Claude Code to use.' : 'Failed: ' + data.error);
|
|
560
|
-
} catch (e) { alert('Error: ' + e.message); }
|
|
561
|
-
}
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
</script>
|
|
565
|
-
</body>
|
|
566
|
-
</html>`;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
349
|
app.listen(PORT, () => {
|
|
570
350
|
console.error(`OpenCode Bridge running on http://localhost:${PORT}`);
|
|
571
|
-
console.error(`Dashboard: http://localhost:${PORT}`);
|
|
572
351
|
});
|