ocb-cli 1.0.4 → 1.0.6
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 +227 -264
- package/package.json +1 -1
- package/src/proxy.ts +228 -264
package/dist/proxy.js
CHANGED
|
@@ -73,7 +73,11 @@ function extractTextFromContent(content) {
|
|
|
73
73
|
async function sendMessage(sessionId, messages) {
|
|
74
74
|
const reversed = [...messages].reverse();
|
|
75
75
|
let lastUserMessage = null;
|
|
76
|
+
let systemPrompt = "";
|
|
76
77
|
for (const m of reversed) {
|
|
78
|
+
if (m.role === "system") {
|
|
79
|
+
systemPrompt = extractTextFromContent(m.content);
|
|
80
|
+
}
|
|
77
81
|
if (m.role === "user") {
|
|
78
82
|
const content = extractTextFromContent(m.content);
|
|
79
83
|
if (content && content.length > 2 && content !== "count") {
|
|
@@ -85,13 +89,19 @@ async function sendMessage(sessionId, messages) {
|
|
|
85
89
|
if (!lastUserMessage)
|
|
86
90
|
return { text: "OK", tokens: 0 };
|
|
87
91
|
const combinedContent = extractTextFromContent(lastUserMessage.content);
|
|
92
|
+
const requestBody = {
|
|
93
|
+
parts: [{ type: "text", text: combinedContent }]
|
|
94
|
+
};
|
|
95
|
+
if (!systemPrompt) {
|
|
96
|
+
requestBody.systemPrompt = "You are Claude Code, an AI assistant built by Anthropic. You are helpful, harmless, and honest. Respond as Claude Code would.";
|
|
97
|
+
}
|
|
88
98
|
const response = await fetch(`${OPENCODE_SERVER_URL}/session/${sessionId}/message`, {
|
|
89
99
|
method: "POST",
|
|
90
100
|
headers: {
|
|
91
101
|
"Content-Type": "application/json",
|
|
92
102
|
...(OPENCODE_PASSWORD ? { "Authorization": `Basic ${Buffer.from(`opencode:${OPENCODE_PASSWORD}`).toString("base64")}` } : {})
|
|
93
103
|
},
|
|
94
|
-
body: JSON.stringify(
|
|
104
|
+
body: JSON.stringify(requestBody)
|
|
95
105
|
});
|
|
96
106
|
const data = await response.json();
|
|
97
107
|
let fullResponse = "";
|
|
@@ -104,6 +114,13 @@ async function sendMessage(sessionId, messages) {
|
|
|
104
114
|
}
|
|
105
115
|
if (data.info?.tokens)
|
|
106
116
|
tokens = data.info.tokens.total || 0;
|
|
117
|
+
fullResponse = fullResponse
|
|
118
|
+
.replace(/I am opencode/gi, "I am Claude Code")
|
|
119
|
+
.replace(/I'm opencode/gi, "I'm Claude Code")
|
|
120
|
+
.replace(/opencode AI/gi, "Claude Code")
|
|
121
|
+
.replace(/OpenCode AI/gi, "Claude Code")
|
|
122
|
+
.replace(/powered by OpenCode/gi, "built by Anthropic")
|
|
123
|
+
.replace(/OpenCode/gi, "Claude Code");
|
|
107
124
|
return { text: fullResponse, tokens };
|
|
108
125
|
}
|
|
109
126
|
const app = express();
|
|
@@ -156,6 +173,29 @@ app.post("/api/refresh-models", async (req, res) => {
|
|
|
156
173
|
await fetchModelsFromOpenCode();
|
|
157
174
|
res.json({ success: true, count: availableModels.length });
|
|
158
175
|
});
|
|
176
|
+
app.post("/api/apply-claude-config", async (req, res) => {
|
|
177
|
+
try {
|
|
178
|
+
const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import("fs");
|
|
179
|
+
const { join, dirname } = await import("path");
|
|
180
|
+
const homedir = process.env.USERPROFILE || process.env.HOME || process.env.HOMEPATH || "";
|
|
181
|
+
const settingsPath = join(homedir, ".claude", "settings.json");
|
|
182
|
+
const settingsDir = dirname(settingsPath);
|
|
183
|
+
if (!existsSync(settingsDir))
|
|
184
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
185
|
+
let settings = {};
|
|
186
|
+
if (existsSync(settingsPath))
|
|
187
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
188
|
+
settings.env = settings.env || {};
|
|
189
|
+
settings.env.ANTHROPIC_BASE_URL = "http://localhost:8300";
|
|
190
|
+
settings.env.ANTHROPIC_API_KEY = "test";
|
|
191
|
+
settings.env.ANTHROPIC_MODEL = currentModel;
|
|
192
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
193
|
+
res.json({ success: true });
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
159
199
|
app.post("/api/reset-stats", (req, res) => {
|
|
160
200
|
totalTokensUsed = 0;
|
|
161
201
|
totalRequests = 0;
|
|
@@ -259,309 +299,232 @@ app.post("/v1/messages/count_tokens", (req, res) => {
|
|
|
259
299
|
const PORT = PROXY_PORT;
|
|
260
300
|
function generateHTML() {
|
|
261
301
|
return `<!DOCTYPE html>
|
|
262
|
-
<html lang="en">
|
|
302
|
+
<html lang="en" data-theme="dark" class="dark">
|
|
263
303
|
<head>
|
|
264
304
|
<meta charset="UTF-8">
|
|
265
305
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
266
306
|
<title>OCB - OpenCode Bridge</title>
|
|
267
307
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
308
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
268
309
|
<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">
|
|
310
|
+
<script>
|
|
311
|
+
tailwind.config = {
|
|
312
|
+
theme: {
|
|
313
|
+
extend: {
|
|
314
|
+
colors: {
|
|
315
|
+
space: { 950: '#0a0a0f', 900: '#121218', 800: '#1a1a24', 700: '#24242e', border: '#2a2a38' },
|
|
316
|
+
neon: { purple: '#a855f7', cyan: '#22d3ee', green: '#22c55e', pink: '#ec4899' }
|
|
317
|
+
},
|
|
318
|
+
fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] }
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
</script>
|
|
269
323
|
<style>
|
|
270
324
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
271
|
-
body { font-family: 'Inter', -apple-system, sans-serif; background: #
|
|
325
|
+
body { font-family: 'Inter', -apple-system, sans-serif; background: #0a0a0f; color: #e4e4e7; min-height: 100vh; }
|
|
326
|
+
[x-cloak] { display: none !important; }
|
|
272
327
|
::-webkit-scrollbar { width: 6px; }
|
|
273
328
|
::-webkit-scrollbar-track { background: transparent; }
|
|
274
329
|
::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
|
|
275
|
-
.
|
|
276
|
-
.
|
|
330
|
+
.nav-item { transition: all 0.2s; }
|
|
331
|
+
.nav-item.active { background: linear-gradient(90deg, rgba(168,85,247,0.2) 0%, transparent 100%); border-left: 2px solid #a855f7; color: white; }
|
|
277
332
|
</style>
|
|
278
333
|
</head>
|
|
279
|
-
<body>
|
|
280
|
-
<div class="
|
|
281
|
-
<
|
|
282
|
-
<div class="
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<h1 class="text-2xl font-bold bg-gradient-to-r from-white to-zinc-400 bg-clip-text text-transparent">OCB</h1>
|
|
287
|
-
<p class="text-xs text-zinc-500 font-medium">OpenCode Bridge</p>
|
|
288
|
-
</div>
|
|
289
|
-
<div class="ml-8 flex items-center gap-2 px-4 py-2 bg-zinc-900 rounded-xl border border-zinc-800">
|
|
290
|
-
<div id="healthDot" class="w-2.5 h-2.5 bg-green-500 rounded-full animate-pulse"></div>
|
|
291
|
-
<span id="connectionStatus" class="text-sm text-zinc-300">Connected</span>
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
<div class="flex items-center gap-3">
|
|
295
|
-
<div class="text-right mr-4">
|
|
296
|
-
<p class="text-xs text-zinc-500">Current Model</p>
|
|
297
|
-
<p id="currentModelDisplay" class="text-sm font-medium text-white">Loading...</p>
|
|
298
|
-
</div>
|
|
299
|
-
<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">
|
|
300
|
-
<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>
|
|
301
|
-
</button>
|
|
302
|
-
</div>
|
|
334
|
+
<body class="overflow-hidden" x-data="app()" x-init="init()">
|
|
335
|
+
<div class="h-14 border-b border-space-border flex items-center px-4 justify-between bg-space-900/80 backdrop-blur-md">
|
|
336
|
+
<div class="flex items-center gap-3">
|
|
337
|
+
<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>
|
|
338
|
+
<div class="flex flex-col">
|
|
339
|
+
<span class="text-sm font-bold tracking-wide text-white">OCB</span>
|
|
340
|
+
<span class="text-[10px] text-gray-500 font-mono">OPENCODE BRIDGE</span>
|
|
303
341
|
</div>
|
|
304
|
-
</
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
<
|
|
309
|
-
<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>
|
|
310
|
-
<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>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="flex items-center gap-4">
|
|
344
|
+
<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'">
|
|
345
|
+
<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>
|
|
346
|
+
<span x-text="connected ? 'Connected' : 'Disconnected'"></span>
|
|
311
347
|
</div>
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
</
|
|
324
|
-
<
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
<
|
|
330
|
-
|
|
348
|
+
<button @click="refresh()" class="p-2 hover:bg-white/5 rounded-lg transition text-gray-400">
|
|
349
|
+
<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>
|
|
350
|
+
</button>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="flex h-[calc(100vh-56px)]">
|
|
354
|
+
<nav class="w-56 bg-space-900 border-r border-space-border flex flex-col">
|
|
355
|
+
<div class="p-4 space-y-1">
|
|
356
|
+
<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':''">
|
|
357
|
+
<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>
|
|
358
|
+
Dashboard
|
|
359
|
+
</button>
|
|
360
|
+
<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':''">
|
|
361
|
+
<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>
|
|
362
|
+
Models
|
|
363
|
+
</button>
|
|
364
|
+
<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':''">
|
|
365
|
+
<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>
|
|
366
|
+
Settings
|
|
367
|
+
</button>
|
|
331
368
|
</div>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
<div class="
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
369
|
+
<div class="mt-auto p-4 border-t border-space-border">
|
|
370
|
+
<div class="text-xs text-gray-500 font-mono">
|
|
371
|
+
<div class="flex justify-between mb-1"><span>Port:</span><span class="text-gray-400">8300</span></div>
|
|
372
|
+
<div class="flex justify-between"><span>OpenCode:</span><span class="text-neon-green">localhost:4096</span></div>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</nav>
|
|
376
|
+
<main class="flex-1 overflow-auto bg-space-950 p-6">
|
|
377
|
+
<div x-show="tab==='dashboard'" x-transition>
|
|
378
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
379
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
380
|
+
<div class="flex items-center gap-3 mb-3">
|
|
381
|
+
<div class="w-10 h-10 bg-neon-purple/20 rounded-xl flex items-center justify-center text-neon-purple">📊</div>
|
|
382
|
+
<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>
|
|
342
383
|
</div>
|
|
343
384
|
</div>
|
|
344
|
-
<div class="bg-
|
|
345
|
-
<div class="flex items-center gap-3 mb-
|
|
346
|
-
<div class="w-10 h-10 bg-
|
|
347
|
-
<div>
|
|
348
|
-
<p class="text-xs text-zinc-500">Total Tokens</p>
|
|
349
|
-
<p id="totalTokens" class="text-2xl font-bold text-white">0</p>
|
|
350
|
-
</div>
|
|
385
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
386
|
+
<div class="flex items-center gap-3 mb-3">
|
|
387
|
+
<div class="w-10 h-10 bg-neon-cyan/20 rounded-xl flex items-center justify-center text-neon-cyan">🎯</div>
|
|
388
|
+
<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>
|
|
351
389
|
</div>
|
|
352
390
|
</div>
|
|
353
|
-
<div class="bg-
|
|
354
|
-
<div class="flex items-center gap-3 mb-
|
|
355
|
-
<div class="w-10 h-10 bg-green
|
|
356
|
-
<div>
|
|
357
|
-
<p class="text-xs text-zinc-500">Session</p>
|
|
358
|
-
<p id="sessionStatus" class="text-lg font-semibold text-green-400">Active</p>
|
|
359
|
-
</div>
|
|
391
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
392
|
+
<div class="flex items-center gap-3 mb-3">
|
|
393
|
+
<div class="w-10 h-10 bg-neon-green/20 rounded-xl flex items-center justify-center text-neon-green">🤖</div>
|
|
394
|
+
<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>
|
|
360
395
|
</div>
|
|
361
396
|
</div>
|
|
362
397
|
</div>
|
|
363
|
-
<div class="bg-
|
|
364
|
-
<h3 class="text-lg font-semibold text-white mb-4">
|
|
365
|
-
<div class="
|
|
366
|
-
<div class="flex justify-between
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
</div>
|
|
370
|
-
<div class="flex justify-between items-center py-2 border-b border-zinc-800">
|
|
371
|
-
<span class="text-zinc-400">Available Models</span>
|
|
372
|
-
<span id="totalModelsDisplay" class="text-white font-medium">-</span>
|
|
373
|
-
</div>
|
|
374
|
-
<div class="flex justify-between items-center py-2 border-b border-zinc-800">
|
|
375
|
-
<span class="text-zinc-400">Providers</span>
|
|
376
|
-
<span id="totalProviders" class="text-white font-medium">-</span>
|
|
377
|
-
</div>
|
|
378
|
-
<div class="flex justify-between items-center py-2">
|
|
379
|
-
<span class="text-zinc-400">OpenCode Server</span>
|
|
380
|
-
<span class="text-green-400 font-medium">localhost:4096</span>
|
|
381
|
-
</div>
|
|
398
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
399
|
+
<h3 class="text-lg font-semibold text-white mb-4">Current Session</h3>
|
|
400
|
+
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
401
|
+
<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>
|
|
402
|
+
<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>
|
|
403
|
+
<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>
|
|
404
|
+
<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>
|
|
382
405
|
</div>
|
|
406
|
+
<div class="mt-4 flex gap-3">
|
|
407
|
+
<button @click="resetSession()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Reset Session</button>
|
|
408
|
+
<button @click="refreshModels()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Refresh Models</button>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
<div x-show="tab==='models'" x-transition style="display:none;">
|
|
413
|
+
<div class="mb-4 flex gap-4">
|
|
414
|
+
<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">
|
|
415
|
+
<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">
|
|
416
|
+
<option value="all">All Providers</option>
|
|
417
|
+
<template x-for="p in providers" :key="p"><option :value="p" x-text="p"></option></template>
|
|
418
|
+
</select>
|
|
383
419
|
</div>
|
|
384
|
-
<div class="
|
|
385
|
-
<
|
|
386
|
-
|
|
420
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 max-h-[calc(100vh-220px)] overflow-y-auto">
|
|
421
|
+
<template x-for="m in filteredModels" :key="m.id">
|
|
422
|
+
<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'">
|
|
423
|
+
<div class="flex items-start justify-between mb-2">
|
|
424
|
+
<h3 class="font-semibold text-white truncate" x-text="m.name"></h3>
|
|
425
|
+
<span x-show="m.id===currentModel" class="text-xs bg-neon-purple text-white px-2 py-0.5 rounded">Active</span>
|
|
426
|
+
</div>
|
|
427
|
+
<p class="text-xs text-gray-500 font-mono truncate mb-2" x-text="m.id"></p>
|
|
428
|
+
<p class="text-xs text-gray-600" x-text="m.provider"></p>
|
|
429
|
+
</div>
|
|
430
|
+
</template>
|
|
387
431
|
</div>
|
|
388
432
|
</div>
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
433
|
+
<div x-show="tab==='settings'" x-transition style="display:none;">
|
|
434
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-6 mb-4">
|
|
392
435
|
<h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
|
|
393
|
-
<p class="text-
|
|
394
|
-
<div class="bg-
|
|
395
|
-
<pre
|
|
396
|
-
"env": {
|
|
397
|
-
"ANTHROPIC_BASE_URL": "http://localhost:8300",
|
|
398
|
-
"ANTHROPIC_API_KEY": "test",
|
|
399
|
-
"ANTHROPIC_MODEL": "claude-sonnet-4-5"
|
|
400
|
-
}
|
|
401
|
-
}</pre>
|
|
436
|
+
<p class="text-gray-400 text-sm mb-4">Configure Claude Code to use OCB as the API endpoint:</p>
|
|
437
|
+
<div class="bg-space-950 rounded-xl p-4 font-mono text-sm text-gray-300 overflow-x-auto mb-4">
|
|
438
|
+
<pre x-text="configJson"></pre>
|
|
402
439
|
</div>
|
|
403
|
-
<button
|
|
404
|
-
<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="
|
|
405
|
-
|
|
440
|
+
<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">
|
|
441
|
+
<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>
|
|
442
|
+
Apply to Claude Code
|
|
406
443
|
</button>
|
|
407
444
|
</div>
|
|
408
|
-
<div class="bg-
|
|
445
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-6">
|
|
409
446
|
<h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
|
|
410
447
|
<div class="space-y-2 font-mono text-sm">
|
|
411
|
-
<div class="flex justify-between py-2 border-b border-
|
|
412
|
-
<div class="flex justify-between py-2 border-b border-
|
|
413
|
-
<div class="flex justify-between py-2 border-b border-
|
|
414
|
-
<div class="flex justify-between py-2 border-b border-
|
|
415
|
-
<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>
|
|
448
|
+
<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>
|
|
449
|
+
<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>
|
|
450
|
+
<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>
|
|
451
|
+
<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>
|
|
416
452
|
</div>
|
|
417
453
|
</div>
|
|
418
454
|
</div>
|
|
419
455
|
</main>
|
|
420
|
-
|
|
421
|
-
<footer class="bg-[#18181b] border-t border-zinc-800 px-6 py-3">
|
|
422
|
-
<div class="max-w-7xl mx-auto flex items-center justify-between text-sm text-zinc-500">
|
|
423
|
-
<div class="flex items-center gap-6">
|
|
424
|
-
<span>OCB v1.0.1</span>
|
|
425
|
-
<span>Proxy Port: <span class="text-white">8300</span></span>
|
|
426
|
-
</div>
|
|
427
|
-
<span>OpenCode Bridge for Claude Code</span>
|
|
428
|
-
</div>
|
|
429
|
-
</footer>
|
|
430
456
|
</div>
|
|
431
457
|
<script>
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (filtered.length === 0) {
|
|
502
|
-
container.innerHTML = '<div class="col-span-full text-center text-zinc-500 py-12">No models found</div>';
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
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('');
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function filterData() {
|
|
509
|
-
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
510
|
-
if (searchTerm) {
|
|
511
|
-
const filtered = allModels.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
|
|
512
|
-
document.getElementById('sectionTitle').textContent = 'Search Results (' + filtered.length + ')';
|
|
513
|
-
renderModels(filtered);
|
|
514
|
-
renderProviders();
|
|
515
|
-
} else {
|
|
516
|
-
selectProvider(currentProvider);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
async function selectModel(modelId) {
|
|
521
|
-
const res = await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
|
|
522
|
-
const data = await res.json();
|
|
523
|
-
if (data.success) {
|
|
524
|
-
currentModelId = modelId;
|
|
525
|
-
loadStatus();
|
|
526
|
-
renderModels(currentProvider === 'all' ? allModels : (groupedModels[currentProvider] || []));
|
|
527
|
-
document.getElementById('configJson').textContent = JSON.stringify({ env: { ANTHROPIC_BASE_URL: "http://localhost:8300", ANTHROPIC_API_KEY: "test", ANTHROPIC_MODEL: modelId } }, null, 2);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
async function loadStatus() {
|
|
532
|
-
const res = await fetch('/api/status');
|
|
533
|
-
const data = await res.json();
|
|
534
|
-
const model = allModels.find(m => m.id === data.currentModel);
|
|
535
|
-
document.getElementById('currentModelDisplay').textContent = model?.name || data.currentModel;
|
|
536
|
-
document.getElementById('healthDot').className = 'w-2.5 h-2.5 rounded-full animate-pulse ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
|
|
537
|
-
document.getElementById('connectionStatus').textContent = data.sessionId === 'active' ? 'Connected' : 'Disconnected';
|
|
538
|
-
document.getElementById('totalRequests').textContent = data.totalRequests.toLocaleString();
|
|
539
|
-
document.getElementById('totalTokens').textContent = data.totalTokensUsed.toLocaleString();
|
|
540
|
-
document.getElementById('sessionStatus').textContent = data.sessionId === 'active' ? 'Active' : 'Inactive';
|
|
541
|
-
document.getElementById('sessionStatus').className = 'text-lg font-semibold ' + (data.sessionId === 'active' ? 'text-green-400' : 'text-red-400');
|
|
542
|
-
document.getElementById('activeModelName').textContent = model?.name || data.currentModel;
|
|
543
|
-
document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models';
|
|
544
|
-
document.getElementById('totalProviders').textContent = Object.keys(groupedModels).length + ' providers';
|
|
545
|
-
currentModelId = data.currentModel;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
async function resetSession() {
|
|
549
|
-
await fetch('/api/reset-session', { method: 'POST' });
|
|
550
|
-
loadStatus();
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
async function refreshModels() {
|
|
554
|
-
await fetch('/api/refresh-models', { method: 'POST' });
|
|
555
|
-
loadModels();
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function updateStats() {
|
|
559
|
-
document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
|
|
458
|
+
function app() {
|
|
459
|
+
return {
|
|
460
|
+
tab: 'dashboard',
|
|
461
|
+
loading: false,
|
|
462
|
+
connected: false,
|
|
463
|
+
search: '',
|
|
464
|
+
selectedProvider: 'all',
|
|
465
|
+
models: [],
|
|
466
|
+
providers: [],
|
|
467
|
+
filteredModels: [],
|
|
468
|
+
currentModel: '',
|
|
469
|
+
currentModelName: '',
|
|
470
|
+
stats: { requests: 0, tokens: 0, sessionId: '', models: 0, providers: 0 },
|
|
471
|
+
configJson: '',
|
|
472
|
+
async init() {
|
|
473
|
+
await this.refresh();
|
|
474
|
+
setInterval(() => this.refresh(), 3000);
|
|
475
|
+
},
|
|
476
|
+
async refresh() {
|
|
477
|
+
this.loading = true;
|
|
478
|
+
try {
|
|
479
|
+
const [statusRes, modelsRes] = await Promise.all([fetch('/api/status'), fetch('/api/models')]);
|
|
480
|
+
const status = await statusRes.json();
|
|
481
|
+
const modelsData = await modelsRes.json();
|
|
482
|
+
this.connected = status.sessionId === 'active';
|
|
483
|
+
this.stats = { requests: status.totalRequests, tokens: status.totalTokensUsed, sessionId: status.sessionId, models: modelsData.models?.length || 0, providers: modelsData.providers?.length || 0 };
|
|
484
|
+
this.currentModel = status.currentModel;
|
|
485
|
+
this.models = modelsData.models || [];
|
|
486
|
+
this.providers = [...new Set(this.models.map(m => m.provider))].sort();
|
|
487
|
+
const current = this.models.find(m => m.id === this.currentModel);
|
|
488
|
+
this.currentModelName = current?.name || this.currentModel;
|
|
489
|
+
this.filterModels();
|
|
490
|
+
this.updateConfig();
|
|
491
|
+
} catch (e) { console.error(e); }
|
|
492
|
+
this.loading = false;
|
|
493
|
+
},
|
|
494
|
+
filterModels() {
|
|
495
|
+
let filtered = this.models;
|
|
496
|
+
if (this.selectedProvider !== 'all') filtered = filtered.filter(m => m.provider === this.selectedProvider);
|
|
497
|
+
if (this.search) {
|
|
498
|
+
const s = this.search.toLowerCase();
|
|
499
|
+
filtered = filtered.filter(m => m.name.toLowerCase().includes(s) || m.id.toLowerCase().includes(s));
|
|
500
|
+
}
|
|
501
|
+
this.filteredModels = filtered.slice(0, 100);
|
|
502
|
+
},
|
|
503
|
+
async selectModel(modelId) {
|
|
504
|
+
await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
|
|
505
|
+
this.currentModel = modelId;
|
|
506
|
+
this.refresh();
|
|
507
|
+
},
|
|
508
|
+
async resetSession() {
|
|
509
|
+
await fetch('/api/reset-session', { method: 'POST' });
|
|
510
|
+
this.refresh();
|
|
511
|
+
},
|
|
512
|
+
async refreshModels() {
|
|
513
|
+
await fetch('/api/refresh-models', { method: 'POST' });
|
|
514
|
+
this.refresh();
|
|
515
|
+
},
|
|
516
|
+
updateConfig() {
|
|
517
|
+
this.configJson = JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'http://localhost:8300', ANTHROPIC_API_KEY: 'test', ANTHROPIC_MODEL: this.currentModel } }, null, 2);
|
|
518
|
+
},
|
|
519
|
+
async applyToClaude() {
|
|
520
|
+
try {
|
|
521
|
+
const response = await fetch('/api/apply-claude-config', { method: 'POST' });
|
|
522
|
+
const data = await response.json();
|
|
523
|
+
alert(data.success ? 'Applied to Claude Code! Restart Claude Code to use.' : 'Failed: ' + data.error);
|
|
524
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
525
|
+
}
|
|
526
|
+
};
|
|
560
527
|
}
|
|
561
|
-
|
|
562
|
-
loadModels();
|
|
563
|
-
loadStatus();
|
|
564
|
-
setInterval(loadStatus, 3000);
|
|
565
528
|
</script>
|
|
566
529
|
</body>
|
|
567
530
|
</html>`;
|
package/package.json
CHANGED
package/src/proxy.ts
CHANGED
|
@@ -93,8 +93,12 @@ function extractTextFromContent(content: any): string {
|
|
|
93
93
|
async function sendMessage(sessionId: string, messages: any[]): Promise<{ text: string; tokens: number }> {
|
|
94
94
|
const reversed = [...messages].reverse();
|
|
95
95
|
let lastUserMessage = null;
|
|
96
|
+
let systemPrompt = "";
|
|
96
97
|
|
|
97
98
|
for (const m of reversed) {
|
|
99
|
+
if (m.role === "system") {
|
|
100
|
+
systemPrompt = extractTextFromContent(m.content);
|
|
101
|
+
}
|
|
98
102
|
if (m.role === "user") {
|
|
99
103
|
const content = extractTextFromContent(m.content);
|
|
100
104
|
if (content && content.length > 2 && content !== "count") {
|
|
@@ -108,13 +112,21 @@ async function sendMessage(sessionId: string, messages: any[]): Promise<{ text:
|
|
|
108
112
|
|
|
109
113
|
const combinedContent = extractTextFromContent(lastUserMessage.content);
|
|
110
114
|
|
|
115
|
+
const requestBody: any = {
|
|
116
|
+
parts: [{ type: "text", text: combinedContent }]
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (!systemPrompt) {
|
|
120
|
+
requestBody.systemPrompt = "You are Claude Code, an AI assistant built by Anthropic. You are helpful, harmless, and honest. Respond as Claude Code would.";
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
const response = await fetch(`${OPENCODE_SERVER_URL}/session/${sessionId}/message`, {
|
|
112
124
|
method: "POST",
|
|
113
125
|
headers: {
|
|
114
126
|
"Content-Type": "application/json",
|
|
115
127
|
...(OPENCODE_PASSWORD ? { "Authorization": `Basic ${Buffer.from(`opencode:${OPENCODE_PASSWORD}`).toString("base64")}` } : {})
|
|
116
128
|
},
|
|
117
|
-
body: JSON.stringify(
|
|
129
|
+
body: JSON.stringify(requestBody)
|
|
118
130
|
});
|
|
119
131
|
|
|
120
132
|
const data = await response.json();
|
|
@@ -130,6 +142,14 @@ async function sendMessage(sessionId: string, messages: any[]): Promise<{ text:
|
|
|
130
142
|
|
|
131
143
|
if (data.info?.tokens) tokens = data.info.tokens.total || 0;
|
|
132
144
|
|
|
145
|
+
fullResponse = fullResponse
|
|
146
|
+
.replace(/I am opencode/gi, "I am Claude Code")
|
|
147
|
+
.replace(/I'm opencode/gi, "I'm Claude Code")
|
|
148
|
+
.replace(/opencode AI/gi, "Claude Code")
|
|
149
|
+
.replace(/OpenCode AI/gi, "Claude Code")
|
|
150
|
+
.replace(/powered by OpenCode/gi, "built by Anthropic")
|
|
151
|
+
.replace(/OpenCode/gi, "Claude Code");
|
|
152
|
+
|
|
133
153
|
return { text: fullResponse, tokens };
|
|
134
154
|
}
|
|
135
155
|
|
|
@@ -188,6 +208,27 @@ app.post("/api/refresh-models", async (req, res) => {
|
|
|
188
208
|
res.json({ success: true, count: availableModels.length });
|
|
189
209
|
});
|
|
190
210
|
|
|
211
|
+
app.post("/api/apply-claude-config", async (req, res) => {
|
|
212
|
+
try {
|
|
213
|
+
const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import("fs");
|
|
214
|
+
const { join, dirname } = await import("path");
|
|
215
|
+
const homedir = process.env.USERPROFILE || process.env.HOME || process.env.HOMEPATH || "";
|
|
216
|
+
const settingsPath = join(homedir, ".claude", "settings.json");
|
|
217
|
+
const settingsDir = dirname(settingsPath);
|
|
218
|
+
if (!existsSync(settingsDir)) mkdirSync(settingsDir, { recursive: true });
|
|
219
|
+
let settings: any = {};
|
|
220
|
+
if (existsSync(settingsPath)) settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
221
|
+
settings.env = settings.env || {};
|
|
222
|
+
settings.env.ANTHROPIC_BASE_URL = "http://localhost:8300";
|
|
223
|
+
settings.env.ANTHROPIC_API_KEY = "test";
|
|
224
|
+
settings.env.ANTHROPIC_MODEL = currentModel;
|
|
225
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
226
|
+
res.json({ success: true });
|
|
227
|
+
} catch (e) {
|
|
228
|
+
res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
191
232
|
app.post("/api/reset-stats", (req, res) => {
|
|
192
233
|
totalTokensUsed = 0;
|
|
193
234
|
totalRequests = 0;
|
|
@@ -298,309 +339,232 @@ const PORT = PROXY_PORT;
|
|
|
298
339
|
|
|
299
340
|
function generateHTML() {
|
|
300
341
|
return `<!DOCTYPE html>
|
|
301
|
-
<html lang="en">
|
|
342
|
+
<html lang="en" data-theme="dark" class="dark">
|
|
302
343
|
<head>
|
|
303
344
|
<meta charset="UTF-8">
|
|
304
345
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
305
346
|
<title>OCB - OpenCode Bridge</title>
|
|
306
347
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
348
|
+
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
307
349
|
<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">
|
|
350
|
+
<script>
|
|
351
|
+
tailwind.config = {
|
|
352
|
+
theme: {
|
|
353
|
+
extend: {
|
|
354
|
+
colors: {
|
|
355
|
+
space: { 950: '#0a0a0f', 900: '#121218', 800: '#1a1a24', 700: '#24242e', border: '#2a2a38' },
|
|
356
|
+
neon: { purple: '#a855f7', cyan: '#22d3ee', green: '#22c55e', pink: '#ec4899' }
|
|
357
|
+
},
|
|
358
|
+
fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
</script>
|
|
308
363
|
<style>
|
|
309
364
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
310
|
-
body { font-family: 'Inter', -apple-system, sans-serif; background: #
|
|
365
|
+
body { font-family: 'Inter', -apple-system, sans-serif; background: #0a0a0f; color: #e4e4e7; min-height: 100vh; }
|
|
366
|
+
[x-cloak] { display: none !important; }
|
|
311
367
|
::-webkit-scrollbar { width: 6px; }
|
|
312
368
|
::-webkit-scrollbar-track { background: transparent; }
|
|
313
369
|
::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
|
|
314
|
-
.
|
|
315
|
-
.
|
|
370
|
+
.nav-item { transition: all 0.2s; }
|
|
371
|
+
.nav-item.active { background: linear-gradient(90deg, rgba(168,85,247,0.2) 0%, transparent 100%); border-left: 2px solid #a855f7; color: white; }
|
|
316
372
|
</style>
|
|
317
373
|
</head>
|
|
318
|
-
<body>
|
|
319
|
-
<div class="
|
|
320
|
-
<
|
|
321
|
-
<div class="
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
<h1 class="text-2xl font-bold bg-gradient-to-r from-white to-zinc-400 bg-clip-text text-transparent">OCB</h1>
|
|
326
|
-
<p class="text-xs text-zinc-500 font-medium">OpenCode Bridge</p>
|
|
327
|
-
</div>
|
|
328
|
-
<div class="ml-8 flex items-center gap-2 px-4 py-2 bg-zinc-900 rounded-xl border border-zinc-800">
|
|
329
|
-
<div id="healthDot" class="w-2.5 h-2.5 bg-green-500 rounded-full animate-pulse"></div>
|
|
330
|
-
<span id="connectionStatus" class="text-sm text-zinc-300">Connected</span>
|
|
331
|
-
</div>
|
|
332
|
-
</div>
|
|
333
|
-
<div class="flex items-center gap-3">
|
|
334
|
-
<div class="text-right mr-4">
|
|
335
|
-
<p class="text-xs text-zinc-500">Current Model</p>
|
|
336
|
-
<p id="currentModelDisplay" class="text-sm font-medium text-white">Loading...</p>
|
|
337
|
-
</div>
|
|
338
|
-
<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">
|
|
339
|
-
<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>
|
|
340
|
-
</button>
|
|
341
|
-
</div>
|
|
374
|
+
<body class="overflow-hidden" x-data="app()" x-init="init()">
|
|
375
|
+
<div class="h-14 border-b border-space-border flex items-center px-4 justify-between bg-space-900/80 backdrop-blur-md">
|
|
376
|
+
<div class="flex items-center gap-3">
|
|
377
|
+
<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>
|
|
378
|
+
<div class="flex flex-col">
|
|
379
|
+
<span class="text-sm font-bold tracking-wide text-white">OCB</span>
|
|
380
|
+
<span class="text-[10px] text-gray-500 font-mono">OPENCODE BRIDGE</span>
|
|
342
381
|
</div>
|
|
343
|
-
</
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
<
|
|
348
|
-
<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>
|
|
349
|
-
<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>
|
|
382
|
+
</div>
|
|
383
|
+
<div class="flex items-center gap-4">
|
|
384
|
+
<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'">
|
|
385
|
+
<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>
|
|
386
|
+
<span x-text="connected ? 'Connected' : 'Disconnected'"></span>
|
|
350
387
|
</div>
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
</
|
|
363
|
-
<
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
<
|
|
369
|
-
|
|
388
|
+
<button @click="refresh()" class="p-2 hover:bg-white/5 rounded-lg transition text-gray-400">
|
|
389
|
+
<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>
|
|
390
|
+
</button>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
<div class="flex h-[calc(100vh-56px)]">
|
|
394
|
+
<nav class="w-56 bg-space-900 border-r border-space-border flex flex-col">
|
|
395
|
+
<div class="p-4 space-y-1">
|
|
396
|
+
<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':''">
|
|
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="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>
|
|
398
|
+
Dashboard
|
|
399
|
+
</button>
|
|
400
|
+
<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':''">
|
|
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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>
|
|
402
|
+
Models
|
|
403
|
+
</button>
|
|
404
|
+
<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':''">
|
|
405
|
+
<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>
|
|
406
|
+
Settings
|
|
407
|
+
</button>
|
|
370
408
|
</div>
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
<div class="
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
409
|
+
<div class="mt-auto p-4 border-t border-space-border">
|
|
410
|
+
<div class="text-xs text-gray-500 font-mono">
|
|
411
|
+
<div class="flex justify-between mb-1"><span>Port:</span><span class="text-gray-400">8300</span></div>
|
|
412
|
+
<div class="flex justify-between"><span>OpenCode:</span><span class="text-neon-green">localhost:4096</span></div>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</nav>
|
|
416
|
+
<main class="flex-1 overflow-auto bg-space-950 p-6">
|
|
417
|
+
<div x-show="tab==='dashboard'" x-transition>
|
|
418
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
419
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
420
|
+
<div class="flex items-center gap-3 mb-3">
|
|
421
|
+
<div class="w-10 h-10 bg-neon-purple/20 rounded-xl flex items-center justify-center text-neon-purple">📊</div>
|
|
422
|
+
<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
423
|
</div>
|
|
382
424
|
</div>
|
|
383
|
-
<div class="bg-
|
|
384
|
-
<div class="flex items-center gap-3 mb-
|
|
385
|
-
<div class="w-10 h-10 bg-
|
|
386
|
-
<div>
|
|
387
|
-
<p class="text-xs text-zinc-500">Total Tokens</p>
|
|
388
|
-
<p id="totalTokens" class="text-2xl font-bold text-white">0</p>
|
|
389
|
-
</div>
|
|
425
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
426
|
+
<div class="flex items-center gap-3 mb-3">
|
|
427
|
+
<div class="w-10 h-10 bg-neon-cyan/20 rounded-xl flex items-center justify-center text-neon-cyan">🎯</div>
|
|
428
|
+
<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>
|
|
390
429
|
</div>
|
|
391
430
|
</div>
|
|
392
|
-
<div class="bg-
|
|
393
|
-
<div class="flex items-center gap-3 mb-
|
|
394
|
-
<div class="w-10 h-10 bg-green
|
|
395
|
-
<div>
|
|
396
|
-
<p class="text-xs text-zinc-500">Session</p>
|
|
397
|
-
<p id="sessionStatus" class="text-lg font-semibold text-green-400">Active</p>
|
|
398
|
-
</div>
|
|
431
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
432
|
+
<div class="flex items-center gap-3 mb-3">
|
|
433
|
+
<div class="w-10 h-10 bg-neon-green/20 rounded-xl flex items-center justify-center text-neon-green">🤖</div>
|
|
434
|
+
<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>
|
|
399
435
|
</div>
|
|
400
436
|
</div>
|
|
401
437
|
</div>
|
|
402
|
-
<div class="bg-
|
|
403
|
-
<h3 class="text-lg font-semibold text-white mb-4">
|
|
404
|
-
<div class="
|
|
405
|
-
<div class="flex justify-between
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
</div>
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
</
|
|
413
|
-
<div class="flex justify-between items-center py-2 border-b border-zinc-800">
|
|
414
|
-
<span class="text-zinc-400">Providers</span>
|
|
415
|
-
<span id="totalProviders" class="text-white font-medium">-</span>
|
|
416
|
-
</div>
|
|
417
|
-
<div class="flex justify-between items-center py-2">
|
|
418
|
-
<span class="text-zinc-400">OpenCode Server</span>
|
|
419
|
-
<span class="text-green-400 font-medium">localhost:4096</span>
|
|
420
|
-
</div>
|
|
438
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-5">
|
|
439
|
+
<h3 class="text-lg font-semibold text-white mb-4">Current Session</h3>
|
|
440
|
+
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
441
|
+
<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>
|
|
442
|
+
<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>
|
|
443
|
+
<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>
|
|
444
|
+
<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>
|
|
445
|
+
</div>
|
|
446
|
+
<div class="mt-4 flex gap-3">
|
|
447
|
+
<button @click="resetSession()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Reset Session</button>
|
|
448
|
+
<button @click="refreshModels()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Refresh Models</button>
|
|
421
449
|
</div>
|
|
422
450
|
</div>
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
451
|
+
</div>
|
|
452
|
+
<div x-show="tab==='models'" x-transition style="display:none;">
|
|
453
|
+
<div class="mb-4 flex gap-4">
|
|
454
|
+
<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">
|
|
455
|
+
<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">
|
|
456
|
+
<option value="all">All Providers</option>
|
|
457
|
+
<template x-for="p in providers" :key="p"><option :value="p" x-text="p"></option></template>
|
|
458
|
+
</select>
|
|
459
|
+
</div>
|
|
460
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 max-h-[calc(100vh-220px)] overflow-y-auto">
|
|
461
|
+
<template x-for="m in filteredModels" :key="m.id">
|
|
462
|
+
<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'">
|
|
463
|
+
<div class="flex items-start justify-between mb-2">
|
|
464
|
+
<h3 class="font-semibold text-white truncate" x-text="m.name"></h3>
|
|
465
|
+
<span x-show="m.id===currentModel" class="text-xs bg-neon-purple text-white px-2 py-0.5 rounded">Active</span>
|
|
466
|
+
</div>
|
|
467
|
+
<p class="text-xs text-gray-500 font-mono truncate mb-2" x-text="m.id"></p>
|
|
468
|
+
<p class="text-xs text-gray-600" x-text="m.provider"></p>
|
|
469
|
+
</div>
|
|
470
|
+
</template>
|
|
426
471
|
</div>
|
|
427
472
|
</div>
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
<div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
|
|
473
|
+
<div x-show="tab==='settings'" x-transition style="display:none;">
|
|
474
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-6 mb-4">
|
|
431
475
|
<h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
|
|
432
|
-
<p class="text-
|
|
433
|
-
<div class="bg-
|
|
434
|
-
<pre
|
|
435
|
-
"env": {
|
|
436
|
-
"ANTHROPIC_BASE_URL": "http://localhost:8300",
|
|
437
|
-
"ANTHROPIC_API_KEY": "test",
|
|
438
|
-
"ANTHROPIC_MODEL": "claude-sonnet-4-5"
|
|
439
|
-
}
|
|
440
|
-
}</pre>
|
|
476
|
+
<p class="text-gray-400 text-sm mb-4">Configure Claude Code to use OCB as the API endpoint:</p>
|
|
477
|
+
<div class="bg-space-950 rounded-xl p-4 font-mono text-sm text-gray-300 overflow-x-auto mb-4">
|
|
478
|
+
<pre x-text="configJson"></pre>
|
|
441
479
|
</div>
|
|
442
|
-
<button
|
|
443
|
-
<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="
|
|
444
|
-
|
|
480
|
+
<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">
|
|
481
|
+
<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>
|
|
482
|
+
Apply to Claude Code
|
|
445
483
|
</button>
|
|
446
484
|
</div>
|
|
447
|
-
<div class="bg-
|
|
485
|
+
<div class="bg-space-900 rounded-xl border border-space-border p-6">
|
|
448
486
|
<h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
|
|
449
487
|
<div class="space-y-2 font-mono text-sm">
|
|
450
|
-
<div class="flex justify-between py-2 border-b border-
|
|
451
|
-
<div class="flex justify-between py-2 border-b border-
|
|
452
|
-
<div class="flex justify-between py-2 border-b border-
|
|
453
|
-
<div class="flex justify-between py-2 border-b border-
|
|
454
|
-
<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>
|
|
488
|
+
<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>
|
|
489
|
+
<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>
|
|
490
|
+
<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>
|
|
491
|
+
<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>
|
|
455
492
|
</div>
|
|
456
493
|
</div>
|
|
457
494
|
</div>
|
|
458
495
|
</main>
|
|
459
|
-
|
|
460
|
-
<footer class="bg-[#18181b] border-t border-zinc-800 px-6 py-3">
|
|
461
|
-
<div class="max-w-7xl mx-auto flex items-center justify-between text-sm text-zinc-500">
|
|
462
|
-
<div class="flex items-center gap-6">
|
|
463
|
-
<span>OCB v1.0.1</span>
|
|
464
|
-
<span>Proxy Port: <span class="text-white">8300</span></span>
|
|
465
|
-
</div>
|
|
466
|
-
<span>OpenCode Bridge for Claude Code</span>
|
|
467
|
-
</div>
|
|
468
|
-
</footer>
|
|
469
496
|
</div>
|
|
470
497
|
<script>
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (filtered.length === 0) {
|
|
541
|
-
container.innerHTML = '<div class="col-span-full text-center text-zinc-500 py-12">No models found</div>';
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
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('');
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function filterData() {
|
|
548
|
-
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
549
|
-
if (searchTerm) {
|
|
550
|
-
const filtered = allModels.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
|
|
551
|
-
document.getElementById('sectionTitle').textContent = 'Search Results (' + filtered.length + ')';
|
|
552
|
-
renderModels(filtered);
|
|
553
|
-
renderProviders();
|
|
554
|
-
} else {
|
|
555
|
-
selectProvider(currentProvider);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
async function selectModel(modelId) {
|
|
560
|
-
const res = await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
|
|
561
|
-
const data = await res.json();
|
|
562
|
-
if (data.success) {
|
|
563
|
-
currentModelId = modelId;
|
|
564
|
-
loadStatus();
|
|
565
|
-
renderModels(currentProvider === 'all' ? allModels : (groupedModels[currentProvider] || []));
|
|
566
|
-
document.getElementById('configJson').textContent = JSON.stringify({ env: { ANTHROPIC_BASE_URL: "http://localhost:8300", ANTHROPIC_API_KEY: "test", ANTHROPIC_MODEL: modelId } }, null, 2);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
async function loadStatus() {
|
|
571
|
-
const res = await fetch('/api/status');
|
|
572
|
-
const data = await res.json();
|
|
573
|
-
const model = allModels.find(m => m.id === data.currentModel);
|
|
574
|
-
document.getElementById('currentModelDisplay').textContent = model?.name || data.currentModel;
|
|
575
|
-
document.getElementById('healthDot').className = 'w-2.5 h-2.5 rounded-full animate-pulse ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
|
|
576
|
-
document.getElementById('connectionStatus').textContent = data.sessionId === 'active' ? 'Connected' : 'Disconnected';
|
|
577
|
-
document.getElementById('totalRequests').textContent = data.totalRequests.toLocaleString();
|
|
578
|
-
document.getElementById('totalTokens').textContent = data.totalTokensUsed.toLocaleString();
|
|
579
|
-
document.getElementById('sessionStatus').textContent = data.sessionId === 'active' ? 'Active' : 'Inactive';
|
|
580
|
-
document.getElementById('sessionStatus').className = 'text-lg font-semibold ' + (data.sessionId === 'active' ? 'text-green-400' : 'text-red-400');
|
|
581
|
-
document.getElementById('activeModelName').textContent = model?.name || data.currentModel;
|
|
582
|
-
document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models';
|
|
583
|
-
document.getElementById('totalProviders').textContent = Object.keys(groupedModels).length + ' providers';
|
|
584
|
-
currentModelId = data.currentModel;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
async function resetSession() {
|
|
588
|
-
await fetch('/api/reset-session', { method: 'POST' });
|
|
589
|
-
loadStatus();
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
async function refreshModels() {
|
|
593
|
-
await fetch('/api/refresh-models', { method: 'POST' });
|
|
594
|
-
loadModels();
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function updateStats() {
|
|
598
|
-
document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
|
|
498
|
+
function app() {
|
|
499
|
+
return {
|
|
500
|
+
tab: 'dashboard',
|
|
501
|
+
loading: false,
|
|
502
|
+
connected: false,
|
|
503
|
+
search: '',
|
|
504
|
+
selectedProvider: 'all',
|
|
505
|
+
models: [],
|
|
506
|
+
providers: [],
|
|
507
|
+
filteredModels: [],
|
|
508
|
+
currentModel: '',
|
|
509
|
+
currentModelName: '',
|
|
510
|
+
stats: { requests: 0, tokens: 0, sessionId: '', models: 0, providers: 0 },
|
|
511
|
+
configJson: '',
|
|
512
|
+
async init() {
|
|
513
|
+
await this.refresh();
|
|
514
|
+
setInterval(() => this.refresh(), 3000);
|
|
515
|
+
},
|
|
516
|
+
async refresh() {
|
|
517
|
+
this.loading = true;
|
|
518
|
+
try {
|
|
519
|
+
const [statusRes, modelsRes] = await Promise.all([fetch('/api/status'), fetch('/api/models')]);
|
|
520
|
+
const status = await statusRes.json();
|
|
521
|
+
const modelsData = await modelsRes.json();
|
|
522
|
+
this.connected = status.sessionId === 'active';
|
|
523
|
+
this.stats = { requests: status.totalRequests, tokens: status.totalTokensUsed, sessionId: status.sessionId, models: modelsData.models?.length || 0, providers: modelsData.providers?.length || 0 };
|
|
524
|
+
this.currentModel = status.currentModel;
|
|
525
|
+
this.models = modelsData.models || [];
|
|
526
|
+
this.providers = [...new Set(this.models.map(m => m.provider))].sort();
|
|
527
|
+
const current = this.models.find(m => m.id === this.currentModel);
|
|
528
|
+
this.currentModelName = current?.name || this.currentModel;
|
|
529
|
+
this.filterModels();
|
|
530
|
+
this.updateConfig();
|
|
531
|
+
} catch (e) { console.error(e); }
|
|
532
|
+
this.loading = false;
|
|
533
|
+
},
|
|
534
|
+
filterModels() {
|
|
535
|
+
let filtered = this.models;
|
|
536
|
+
if (this.selectedProvider !== 'all') filtered = filtered.filter(m => m.provider === this.selectedProvider);
|
|
537
|
+
if (this.search) {
|
|
538
|
+
const s = this.search.toLowerCase();
|
|
539
|
+
filtered = filtered.filter(m => m.name.toLowerCase().includes(s) || m.id.toLowerCase().includes(s));
|
|
540
|
+
}
|
|
541
|
+
this.filteredModels = filtered.slice(0, 100);
|
|
542
|
+
},
|
|
543
|
+
async selectModel(modelId) {
|
|
544
|
+
await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
|
|
545
|
+
this.currentModel = modelId;
|
|
546
|
+
this.refresh();
|
|
547
|
+
},
|
|
548
|
+
async resetSession() {
|
|
549
|
+
await fetch('/api/reset-session', { method: 'POST' });
|
|
550
|
+
this.refresh();
|
|
551
|
+
},
|
|
552
|
+
async refreshModels() {
|
|
553
|
+
await fetch('/api/refresh-models', { method: 'POST' });
|
|
554
|
+
this.refresh();
|
|
555
|
+
},
|
|
556
|
+
updateConfig() {
|
|
557
|
+
this.configJson = JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'http://localhost:8300', ANTHROPIC_API_KEY: 'test', ANTHROPIC_MODEL: this.currentModel } }, null, 2);
|
|
558
|
+
},
|
|
559
|
+
async applyToClaude() {
|
|
560
|
+
try {
|
|
561
|
+
const response = await fetch('/api/apply-claude-config', { method: 'POST' });
|
|
562
|
+
const data = await response.json();
|
|
563
|
+
alert(data.success ? 'Applied to Claude Code! Restart Claude Code to use.' : 'Failed: ' + data.error);
|
|
564
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
565
|
+
}
|
|
566
|
+
};
|
|
599
567
|
}
|
|
600
|
-
|
|
601
|
-
loadModels();
|
|
602
|
-
loadStatus();
|
|
603
|
-
setInterval(loadStatus, 3000);
|
|
604
568
|
</script>
|
|
605
569
|
</body>
|
|
606
570
|
</html>`;
|