ocb-cli 1.0.0 → 1.0.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/proxy.js +97 -239
- package/package.json +1 -1
- package/src/proxy.ts +98 -247
package/dist/proxy.js
CHANGED
|
@@ -132,11 +132,7 @@ app.get("/api/models", (req, res) => {
|
|
|
132
132
|
grouped[model.provider] = [];
|
|
133
133
|
grouped[model.provider].push(model);
|
|
134
134
|
}
|
|
135
|
-
res.json({
|
|
136
|
-
models: availableModels,
|
|
137
|
-
grouped,
|
|
138
|
-
providers: Object.entries(providers).map(([id, p]) => ({ id, ...p }))
|
|
139
|
-
});
|
|
135
|
+
res.json({ models: availableModels, grouped, providers: Object.entries(providers).map(([id, p]) => ({ id, ...p })) });
|
|
140
136
|
});
|
|
141
137
|
app.post("/api/model", (req, res) => {
|
|
142
138
|
const { modelId } = req.body;
|
|
@@ -165,332 +161,194 @@ app.post("/api/reset-session", async (req, res) => {
|
|
|
165
161
|
});
|
|
166
162
|
app.get("/v1/authenticate", (req, res) => res.json({ type: "authentication", authenticated: true }));
|
|
167
163
|
app.get("/v1/whoami", (req, res) => res.json({ type: "user", id: "opencode-user", email: "opencode@local" }));
|
|
168
|
-
app.get("/v1/models", (req, res) => {
|
|
169
|
-
|
|
170
|
-
id: m.id, type: "model", name: m.name,
|
|
171
|
-
supports_cached_previews: true, supports_system_instructions: true
|
|
172
|
-
})) });
|
|
173
|
-
});
|
|
174
|
-
app.get("/v1/models/list", (req, res) => {
|
|
175
|
-
res.json({ data: availableModels.map(m => ({
|
|
176
|
-
id: m.id, type: "model", name: m.name,
|
|
177
|
-
supports_cached_previews: true, supports_system_instructions: true
|
|
178
|
-
})) });
|
|
179
|
-
});
|
|
164
|
+
app.get("/v1/models", (req, res) => res.json({ data: availableModels.map(m => ({ id: m.id, type: "model", name: m.name, supports_cached_previews: true, supports_system_instructions: true })) }));
|
|
165
|
+
app.get("/v1/models/list", (req, res) => res.json({ data: availableModels.map(m => ({ id: m.id, type: "model", name: m.name, supports_cached_previews: true, supports_system_instructions: true })) }));
|
|
180
166
|
app.post("/v1/messages", async (req, res) => {
|
|
181
167
|
try {
|
|
182
168
|
if (req.body?.max_tokens === undefined && req.body?.messages) {
|
|
183
|
-
const messages = req.body.messages;
|
|
184
169
|
let totalTokens = 0;
|
|
185
|
-
for (const msg of messages) {
|
|
186
|
-
|
|
187
|
-
totalTokens += Math.ceil(content.length / 4);
|
|
170
|
+
for (const msg of req.body.messages) {
|
|
171
|
+
totalTokens += Math.ceil(extractTextFromContent(msg.content).length / 4);
|
|
188
172
|
}
|
|
189
173
|
return res.json({ tokens: totalTokens });
|
|
190
174
|
}
|
|
191
|
-
|
|
192
|
-
if (!currentSessionId) {
|
|
175
|
+
if (!currentSessionId)
|
|
193
176
|
currentSessionId = await createSession(process.cwd());
|
|
194
|
-
}
|
|
195
|
-
const { text, tokens } = await sendMessage(currentSessionId, messages);
|
|
177
|
+
const { text, tokens } = await sendMessage(currentSessionId, req.body.messages);
|
|
196
178
|
totalRequests++;
|
|
197
179
|
totalTokensUsed += tokens;
|
|
198
|
-
res.json({
|
|
199
|
-
id: `msg_${Date.now()}`,
|
|
200
|
-
type: "message",
|
|
201
|
-
role: "assistant",
|
|
202
|
-
content: [{ type: "text", text }],
|
|
203
|
-
model: currentModel,
|
|
204
|
-
stop_reason: "end_turn",
|
|
205
|
-
usage: {
|
|
206
|
-
input_tokens: Math.ceil((JSON.stringify(messages).length) / 4),
|
|
207
|
-
output_tokens: Math.ceil(text.length / 4)
|
|
208
|
-
}
|
|
209
|
-
});
|
|
180
|
+
res.json({ id: `msg_${Date.now()}`, type: "message", role: "assistant", content: [{ type: "text", text }], model: currentModel, stop_reason: "end_turn", usage: { input_tokens: Math.ceil(JSON.stringify(req.body.messages).length / 4), output_tokens: Math.ceil(text.length / 4) } });
|
|
210
181
|
}
|
|
211
182
|
catch (error) {
|
|
212
|
-
console.error("Error:", error);
|
|
213
183
|
res.status(500).json({ error: { type: "api_error", message: error instanceof Error ? error.message : String(error) } });
|
|
214
184
|
}
|
|
215
185
|
});
|
|
216
186
|
app.post("/v1/messages/count_tokens", (req, res) => {
|
|
217
|
-
const { messages } = req.body;
|
|
218
187
|
let totalTokens = 0;
|
|
219
|
-
for (const msg of messages || [])
|
|
220
|
-
|
|
221
|
-
totalTokens += Math.ceil(content.length / 4);
|
|
222
|
-
}
|
|
188
|
+
for (const msg of req.body.messages || [])
|
|
189
|
+
totalTokens += Math.ceil(extractTextFromContent(msg.content).length / 4);
|
|
223
190
|
res.json({ tokens: totalTokens });
|
|
224
191
|
});
|
|
192
|
+
const PORT = PROXY_PORT;
|
|
225
193
|
function generateHTML() {
|
|
226
194
|
return `<!DOCTYPE html>
|
|
227
195
|
<html lang="en">
|
|
228
196
|
<head>
|
|
229
197
|
<meta charset="UTF-8">
|
|
230
198
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
231
|
-
<title>OpenCode Bridge</title>
|
|
199
|
+
<title>OCB - OpenCode Bridge</title>
|
|
232
200
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
233
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
201
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
234
202
|
<style>
|
|
235
|
-
* {
|
|
236
|
-
body { font-family: 'Inter', -apple-system, sans-serif; background: #
|
|
237
|
-
|
|
238
|
-
.app-container { display: flex; height: 100vh; }
|
|
239
|
-
|
|
240
|
-
/* Sidebar */
|
|
241
|
-
.sidebar { width: 280px; background: #121218; border-right: 1px solid #27272a; display: flex; flex-direction: column; }
|
|
242
|
-
.sidebar-header { padding: 20px; border-bottom: 1px solid #27272a; }
|
|
243
|
-
.logo { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 16px; }
|
|
244
|
-
.logo-icon { width: 28px; height: 28px; background: linear-gradient(135deg, #6366f1, #8b5cf6); border-radius: 6px; display: flex; align-items: center; justify-content: center; }
|
|
245
|
-
|
|
246
|
-
/* Search */
|
|
247
|
-
.search-box { padding: 16px 20px; }
|
|
248
|
-
.search-input { width: 100%; padding: 10px 12px; background: #1a1a20; border: 1px solid #27272a; border-radius: 8px; color: #e4e4e7; font-size: 13px; outline: none; transition: border-color 0.2s; }
|
|
249
|
-
.search-input:focus { border-color: #6366f1; }
|
|
250
|
-
.search-input::placeholder { color: #71717a; }
|
|
251
|
-
|
|
252
|
-
/* Provider List */
|
|
253
|
-
.provider-list { flex: 1; overflow-y: auto; padding: 8px; }
|
|
254
|
-
.provider-item { padding: 10px 12px; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; font-size: 13px; transition: background 0.15s; }
|
|
255
|
-
.provider-item:hover { background: #1f1f26; }
|
|
256
|
-
.provider-item.active { background: #6366f1/15; color: #a5b4fc; }
|
|
257
|
-
.provider-name { display: flex; align-items: center; gap: 8px; }
|
|
258
|
-
.provider-count { font-size: 11px; color: #71717a; background: #27272a; padding: 2px 6px; border-radius: 4px; }
|
|
259
|
-
|
|
260
|
-
/* Main Content */
|
|
261
|
-
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
262
|
-
|
|
263
|
-
/* Header */
|
|
264
|
-
.main-header { padding: 16px 24px; border-bottom: 1px solid #27272a; display: flex; align-items: center; justify-content: space-between; background: #121218; }
|
|
265
|
-
.header-left { display: flex; align-items: center; gap: 16px; }
|
|
266
|
-
.current-model-badge { display: flex; align-items: center; gap: 8px; padding: 8px 14px; background: #6366f1/15; border: 1px solid #6366f1/30; border-radius: 8px; font-size: 13px; }
|
|
267
|
-
.status-dot { width: 8px; height: 8px; background: #22c55e; border-radius: 50%; }
|
|
268
|
-
.status-dot.inactive { background: #ef4444; }
|
|
269
|
-
|
|
270
|
-
/* Model List */
|
|
271
|
-
.model-list-container { flex: 1; overflow-y: auto; padding: 16px 24px; }
|
|
272
|
-
.model-list-header { font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: #71717a; margin-bottom: 12px; padding: 0 8px; }
|
|
273
|
-
.model-item { padding: 12px 16px; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; transition: all 0.15s; border: 1px solid transparent; margin-bottom: 4px; }
|
|
274
|
-
.model-item:hover { background: #1f1f26; }
|
|
275
|
-
.model-item.selected { background: #6366f1/15; border-color: #6366f1/40; }
|
|
276
|
-
.model-info { display: flex; flex-direction: column; gap: 2px; }
|
|
277
|
-
.model-name { font-size: 14px; font-weight: 500; }
|
|
278
|
-
.model-id { font-size: 11px; color: #71717a; font-family: 'JetBrains Mono', monospace; }
|
|
279
|
-
.model-cost { font-size: 11px; color: #a1a1aa; text-align: right; }
|
|
280
|
-
.model-cost span { display: block; }
|
|
281
|
-
|
|
282
|
-
/* Footer Stats */
|
|
283
|
-
.footer { padding: 12px 24px; border-top: 1px solid #27272a; display: flex; gap: 24px; background: #121218; }
|
|
284
|
-
.stat-item { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #a1a1aa; }
|
|
285
|
-
.stat-value { color: #e4e4e7; font-weight: 500; }
|
|
286
|
-
|
|
287
|
-
/* Scrollbar */
|
|
203
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
204
|
+
body { font-family: 'Inter', -apple-system, sans-serif; background: #09090b; color: #fafafa; min-height: 100vh; }
|
|
288
205
|
::-webkit-scrollbar { width: 6px; }
|
|
289
206
|
::-webkit-scrollbar-track { background: transparent; }
|
|
290
207
|
::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
|
|
291
|
-
::-webkit-scrollbar-thumb:hover { background: #52525b; }
|
|
292
|
-
|
|
293
|
-
/* Buttons */
|
|
294
|
-
.btn { padding: 8px 14px; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.15s; border: none; }
|
|
295
|
-
.btn-primary { background: #6366f1; color: white; }
|
|
296
|
-
.btn-primary:hover { background: #4f46e5; }
|
|
297
|
-
.btn-secondary { background: #27272a; color: #a1a1aa; }
|
|
298
|
-
.btn-secondary:hover { background: #3f3f46; color: #e4e4e7; }
|
|
299
208
|
</style>
|
|
300
209
|
</head>
|
|
301
210
|
<body>
|
|
302
|
-
<div class="
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
<div
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
</div>
|
|
311
|
-
|
|
312
|
-
<div class="search-box">
|
|
313
|
-
<input type="text" class="search-input" placeholder="Search models..." id="searchInput" oninput="filterModels()">
|
|
314
|
-
</div>
|
|
315
|
-
|
|
316
|
-
<div class="provider-list" id="providerList">
|
|
317
|
-
<!-- Providers loaded here -->
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
320
|
-
|
|
321
|
-
<!-- Main Content -->
|
|
322
|
-
<div class="main-content">
|
|
323
|
-
<div class="main-header">
|
|
324
|
-
<div class="header-left">
|
|
325
|
-
<div class="current-model-badge">
|
|
326
|
-
<div class="status-dot" id="statusDot"></div>
|
|
327
|
-
<span id="currentModelName">Loading...</span>
|
|
211
|
+
<div class="min-h-screen flex flex-col">
|
|
212
|
+
<header class="bg-zinc-900 border-b border-zinc-800 px-6 py-4">
|
|
213
|
+
<div class="max-w-7xl mx-auto flex items-center justify-between">
|
|
214
|
+
<div class="flex items-center gap-3">
|
|
215
|
+
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center text-xl font-bold">⚡</div>
|
|
216
|
+
<div>
|
|
217
|
+
<h1 class="text-xl font-semibold text-white">OCB</h1>
|
|
218
|
+
<p class="text-xs text-zinc-500">OpenCode Bridge</p>
|
|
328
219
|
</div>
|
|
329
220
|
</div>
|
|
330
|
-
<div
|
|
331
|
-
<
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
<div class="model-list-header" id="modelListHeader">All Models</div>
|
|
338
|
-
<div id="modelList">
|
|
339
|
-
<!-- Models loaded here -->
|
|
221
|
+
<div class="flex items-center gap-3">
|
|
222
|
+
<div class="flex items-center gap-2 px-3 py-1.5 bg-zinc-800 rounded-lg">
|
|
223
|
+
<div id="statusDot" class="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
224
|
+
<span id="currentModelDisplay" class="text-sm text-zinc-300">Loading...</span>
|
|
225
|
+
</div>
|
|
226
|
+
<button onclick="refreshModels()" class="px-3 py-1.5 text-sm bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition">↻</button>
|
|
227
|
+
<button onclick="resetSession()" class="px-3 py-1.5 text-sm bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition">Reset</button>
|
|
340
228
|
</div>
|
|
341
229
|
</div>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<
|
|
230
|
+
</header>
|
|
231
|
+
<main class="flex-1 flex max-w-7xl mx-auto w-full">
|
|
232
|
+
<aside class="w-64 bg-zinc-900 border-r border-zinc-800 flex flex-col">
|
|
233
|
+
<div class="p-4">
|
|
234
|
+
<input type="text" id="searchInput" placeholder="Search models..." oninput="filterData()" class="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-indigo-500">
|
|
347
235
|
</div>
|
|
348
|
-
<div class="
|
|
349
|
-
|
|
350
|
-
|
|
236
|
+
<div id="providerList" class="flex-1 overflow-y-auto px-2 pb-4"></div>
|
|
237
|
+
</aside>
|
|
238
|
+
<section class="flex-1 p-6 overflow-y-auto">
|
|
239
|
+
<div class="flex items-center justify-between mb-4">
|
|
240
|
+
<h2 id="sectionTitle" class="text-lg font-medium text-white">All Models</h2>
|
|
241
|
+
<span id="modelCount" class="text-sm text-zinc-500"></span>
|
|
351
242
|
</div>
|
|
352
|
-
<div class="
|
|
353
|
-
|
|
354
|
-
|
|
243
|
+
<div id="modelGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"></div>
|
|
244
|
+
</section>
|
|
245
|
+
</main>
|
|
246
|
+
<footer class="bg-zinc-900 border-t border-zinc-800 px-6 py-3">
|
|
247
|
+
<div class="max-w-7xl mx-auto flex items-center justify-between text-sm text-zinc-500">
|
|
248
|
+
<div class="flex items-center gap-6">
|
|
249
|
+
<span>Requests: <span id="totalRequests" class="text-white font-medium">0</span></span>
|
|
250
|
+
<span>Tokens: <span id="totalTokens" class="text-white font-medium">0</span></span>
|
|
355
251
|
</div>
|
|
252
|
+
<span id="totalModelsDisplay">Loading...</span>
|
|
356
253
|
</div>
|
|
357
|
-
</
|
|
254
|
+
</footer>
|
|
358
255
|
</div>
|
|
359
|
-
|
|
360
256
|
<script>
|
|
361
|
-
const PROXY_PORT = ${PROXY_PORT};
|
|
362
257
|
let allModels = [];
|
|
363
258
|
let groupedModels = {};
|
|
364
259
|
let currentProvider = 'all';
|
|
365
260
|
let currentModelId = null;
|
|
366
|
-
|
|
261
|
+
|
|
367
262
|
async function loadModels() {
|
|
368
263
|
const res = await fetch('/api/models');
|
|
369
264
|
const data = await res.json();
|
|
370
|
-
|
|
371
265
|
allModels = data.models || [];
|
|
372
266
|
groupedModels = data.grouped || {};
|
|
373
|
-
|
|
374
267
|
renderProviders();
|
|
375
268
|
renderModels(allModels);
|
|
376
269
|
updateStats();
|
|
377
270
|
}
|
|
378
|
-
|
|
271
|
+
|
|
379
272
|
function renderProviders() {
|
|
380
273
|
const container = document.getElementById('providerList');
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
</div
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
html += \`<div class="provider-item \${currentProvider === provider ? 'active' : ''}" onclick="selectProvider('\${provider}')">
|
|
390
|
-
<span class="provider-name">📦 \${provider}</span>
|
|
391
|
-
<span class="provider-count">\${models.length}</span>
|
|
392
|
-
</div>\`;
|
|
274
|
+
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
275
|
+
let providersToShow = Object.entries(groupedModels);
|
|
276
|
+
if (searchTerm) {
|
|
277
|
+
providersToShow = providersToShow.filter(([name, models]) => name.toLowerCase().includes(searchTerm) || models.some(m => m.name.toLowerCase().includes(searchTerm)));
|
|
278
|
+
}
|
|
279
|
+
let html = '<div onclick="selectProvider('all')" class="px-3 py-2 rounded-lg cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === 'all' ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span>🏠 All</span><span class="text-xs opacity-60">' + allModels.length + '</span></div>';
|
|
280
|
+
for (const [provider, models] of providersToShow) {
|
|
281
|
+
html += '<div onclick="selectProvider(\\'' + provider + '\\')" class="px-3 py-2 rounded-lg cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === provider ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span class="truncate">' + provider + '</span><span class="text-xs opacity-60">' + models.length + '</span></div>';
|
|
393
282
|
}
|
|
394
|
-
|
|
395
283
|
container.innerHTML = html;
|
|
396
284
|
}
|
|
397
|
-
|
|
285
|
+
|
|
398
286
|
function selectProvider(provider) {
|
|
399
287
|
currentProvider = provider;
|
|
288
|
+
document.getElementById('sectionTitle').textContent = provider === 'all' ? 'All Models' : provider;
|
|
400
289
|
renderProviders();
|
|
401
|
-
|
|
402
|
-
const models = provider === 'all' ? allModels : groupedModels[provider] || [];
|
|
403
|
-
document.getElementById('modelListHeader').textContent = provider === 'all' ? 'All Models' : provider;
|
|
404
|
-
renderModels(models);
|
|
290
|
+
renderModels(provider === 'all' ? allModels : (groupedModels[provider] || []));
|
|
405
291
|
}
|
|
406
|
-
|
|
292
|
+
|
|
407
293
|
function renderModels(models) {
|
|
408
|
-
const container = document.getElementById('
|
|
294
|
+
const container = document.getElementById('modelGrid');
|
|
409
295
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
410
|
-
|
|
411
296
|
let filtered = models;
|
|
412
297
|
if (searchTerm) {
|
|
413
|
-
filtered = models.filter(m =>
|
|
414
|
-
m.name.toLowerCase().includes(searchTerm) ||
|
|
415
|
-
m.id.toLowerCase().includes(searchTerm) ||
|
|
416
|
-
m.provider.toLowerCase().includes(searchTerm)
|
|
417
|
-
);
|
|
298
|
+
filtered = models.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
|
|
418
299
|
}
|
|
419
|
-
|
|
300
|
+
document.getElementById('modelCount').textContent = filtered.length + ' models';
|
|
420
301
|
if (filtered.length === 0) {
|
|
421
|
-
container.innerHTML = '<div
|
|
302
|
+
container.innerHTML = '<div class="col-span-full text-center text-zinc-500 py-12">No models found</div>';
|
|
422
303
|
return;
|
|
423
304
|
}
|
|
424
|
-
|
|
425
|
-
let html = '';
|
|
426
|
-
for (const model of filtered) {
|
|
427
|
-
const isSelected = model.id === currentModelId;
|
|
428
|
-
const costInfo = model.cost ? \`<span>$\${model.cost.input}/M in</span><span>$\${model.cost.output}/M out</span>\` : '';
|
|
429
|
-
|
|
430
|
-
html += \`<div class="model-item \${isSelected ? 'selected' : ''}" onclick="selectModel('\${model.id}')">
|
|
431
|
-
<div class="model-info">
|
|
432
|
-
<div class="model-name">\${model.name}</div>
|
|
433
|
-
<div class="model-id">\${model.id}</div>
|
|
434
|
-
</div>
|
|
435
|
-
<div class="model-cost">\${costInfo}</div>
|
|
436
|
-
</div>\`;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
container.innerHTML = html;
|
|
305
|
+
container.innerHTML = filtered.map(m => '<div onclick="selectModel(\\'' + m.id + '\\')" class="p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02] ' + (m.id === currentModelId ? 'bg-indigo-600/20 border-indigo-500' : 'bg-zinc-900 border-zinc-800 hover:border-zinc-700') + '"><div class="flex items-start justify-between mb-2"><h3 class="font-medium text-white truncate">' + m.name + '</h3>' + (m.id === currentModelId ? '<span class="text-xs bg-indigo-500 text-white px-2 py-0.5 rounded">Active</span>' : '') + '</div><p class="text-xs text-zinc-500 font-mono truncate mb-2">' + m.id + '</p>' + (m.cost ? '<p class="text-xs text-zinc-400">$' + m.cost.input + '/M → $' + m.cost.output + '/M</p>' : '') + '</div>').join('');
|
|
440
306
|
}
|
|
441
|
-
|
|
442
|
-
function
|
|
443
|
-
const
|
|
444
|
-
|
|
307
|
+
|
|
308
|
+
function filterData() {
|
|
309
|
+
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
310
|
+
if (searchTerm) {
|
|
311
|
+
const filtered = allModels.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
|
|
312
|
+
document.getElementById('sectionTitle').textContent = 'Search Results';
|
|
313
|
+
renderModels(filtered);
|
|
314
|
+
renderProviders();
|
|
315
|
+
} else {
|
|
316
|
+
selectProvider(currentProvider);
|
|
317
|
+
}
|
|
445
318
|
}
|
|
446
|
-
|
|
319
|
+
|
|
447
320
|
async function selectModel(modelId) {
|
|
448
|
-
const res = await fetch('/api/model', {
|
|
449
|
-
method: 'POST',
|
|
450
|
-
headers: { 'Content-Type': 'application/json' },
|
|
451
|
-
body: JSON.stringify({ modelId })
|
|
452
|
-
});
|
|
321
|
+
const res = await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
|
|
453
322
|
const data = await res.json();
|
|
454
323
|
if (data.success) {
|
|
455
324
|
currentModelId = modelId;
|
|
456
325
|
loadStatus();
|
|
457
|
-
|
|
458
|
-
renderModels(models);
|
|
326
|
+
renderModels(currentProvider === 'all' ? allModels : (groupedModels[currentProvider] || []));
|
|
459
327
|
}
|
|
460
328
|
}
|
|
461
|
-
|
|
329
|
+
|
|
462
330
|
async function loadStatus() {
|
|
463
331
|
const res = await fetch('/api/status');
|
|
464
332
|
const data = await res.json();
|
|
465
|
-
|
|
466
333
|
const model = allModels.find(m => m.id === data.currentModel);
|
|
467
|
-
document.getElementById('
|
|
468
|
-
|
|
469
|
-
const statusDot = document.getElementById('statusDot');
|
|
470
|
-
if (data.sessionId === 'active') {
|
|
471
|
-
statusDot.classList.remove('inactive');
|
|
472
|
-
} else {
|
|
473
|
-
statusDot.classList.add('inactive');
|
|
474
|
-
}
|
|
475
|
-
|
|
334
|
+
document.getElementById('currentModelDisplay').textContent = model?.name || data.currentModel;
|
|
335
|
+
document.getElementById('statusDot').className = 'w-2 h-2 rounded-full ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
|
|
476
336
|
document.getElementById('totalRequests').textContent = data.totalRequests.toLocaleString();
|
|
477
337
|
document.getElementById('totalTokens').textContent = data.totalTokensUsed.toLocaleString();
|
|
478
|
-
document.getElementById('
|
|
479
|
-
|
|
338
|
+
document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
|
|
480
339
|
currentModelId = data.currentModel;
|
|
481
340
|
}
|
|
482
|
-
|
|
341
|
+
|
|
483
342
|
async function resetSession() {
|
|
484
343
|
await fetch('/api/reset-session', { method: 'POST' });
|
|
485
344
|
loadStatus();
|
|
486
345
|
}
|
|
487
|
-
|
|
488
|
-
async function
|
|
489
|
-
await fetch('/api/
|
|
490
|
-
|
|
346
|
+
|
|
347
|
+
async function refreshModels() {
|
|
348
|
+
await fetch('/api/refresh-models', { method: 'POST' });
|
|
349
|
+
loadModels();
|
|
491
350
|
}
|
|
492
|
-
|
|
493
|
-
// Initialize
|
|
351
|
+
|
|
494
352
|
loadModels();
|
|
495
353
|
loadStatus();
|
|
496
354
|
setInterval(loadStatus, 3000);
|
|
@@ -498,7 +356,7 @@ function generateHTML() {
|
|
|
498
356
|
</body>
|
|
499
357
|
</html>`;
|
|
500
358
|
}
|
|
501
|
-
app.listen(
|
|
502
|
-
console.error(`OpenCode Bridge running on http://localhost:${
|
|
503
|
-
console.error(`Dashboard: http://localhost:${
|
|
359
|
+
app.listen(PORT, () => {
|
|
360
|
+
console.error(`OpenCode Bridge running on http://localhost:${PORT}`);
|
|
361
|
+
console.error(`Dashboard: http://localhost:${PORT}`);
|
|
504
362
|
});
|
package/package.json
CHANGED
package/src/proxy.ts
CHANGED
|
@@ -160,11 +160,7 @@ app.get("/api/models", (req, res) => {
|
|
|
160
160
|
if (!grouped[model.provider]) grouped[model.provider] = [];
|
|
161
161
|
grouped[model.provider].push(model);
|
|
162
162
|
}
|
|
163
|
-
res.json({
|
|
164
|
-
models: availableModels,
|
|
165
|
-
grouped,
|
|
166
|
-
providers: Object.entries(providers).map(([id, p]) => ({ id, ...p }))
|
|
167
|
-
});
|
|
163
|
+
res.json({ models: availableModels, grouped, providers: Object.entries(providers).map(([id, p]) => ({ id, ...p })) });
|
|
168
164
|
});
|
|
169
165
|
|
|
170
166
|
app.post("/api/model", (req, res) => {
|
|
@@ -198,340 +194,195 @@ app.post("/api/reset-session", async (req, res) => {
|
|
|
198
194
|
app.get("/v1/authenticate", (req, res) => res.json({ type: "authentication", authenticated: true }));
|
|
199
195
|
app.get("/v1/whoami", (req, res) => res.json({ type: "user", id: "opencode-user", email: "opencode@local" }));
|
|
200
196
|
|
|
201
|
-
app.get("/v1/models", (req, res) => {
|
|
202
|
-
|
|
203
|
-
id: m.id, type: "model", name: m.name,
|
|
204
|
-
supports_cached_previews: true, supports_system_instructions: true
|
|
205
|
-
}))});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
app.get("/v1/models/list", (req, res) => {
|
|
209
|
-
res.json({ data: availableModels.map(m => ({
|
|
210
|
-
id: m.id, type: "model", name: m.name,
|
|
211
|
-
supports_cached_previews: true, supports_system_instructions: true
|
|
212
|
-
}))});
|
|
213
|
-
});
|
|
197
|
+
app.get("/v1/models", (req, res) => res.json({ data: availableModels.map(m => ({ id: m.id, type: "model", name: m.name, supports_cached_previews: true, supports_system_instructions: true })) }));
|
|
198
|
+
app.get("/v1/models/list", (req, res) => res.json({ data: availableModels.map(m => ({ id: m.id, type: "model", name: m.name, supports_cached_previews: true, supports_system_instructions: true })) }));
|
|
214
199
|
|
|
215
200
|
app.post("/v1/messages", async (req, res) => {
|
|
216
201
|
try {
|
|
217
202
|
if (req.body?.max_tokens === undefined && req.body?.messages) {
|
|
218
|
-
const messages = req.body.messages;
|
|
219
203
|
let totalTokens = 0;
|
|
220
|
-
for (const msg of messages) {
|
|
221
|
-
|
|
222
|
-
totalTokens += Math.ceil(content.length / 4);
|
|
204
|
+
for (const msg of req.body.messages) {
|
|
205
|
+
totalTokens += Math.ceil(extractTextFromContent(msg.content).length / 4);
|
|
223
206
|
}
|
|
224
207
|
return res.json({ tokens: totalTokens });
|
|
225
208
|
}
|
|
226
|
-
|
|
227
|
-
const {
|
|
228
|
-
|
|
229
|
-
if (!currentSessionId) {
|
|
230
|
-
currentSessionId = await createSession(process.cwd());
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const { text, tokens } = await sendMessage(currentSessionId, messages);
|
|
234
|
-
|
|
209
|
+
if (!currentSessionId) currentSessionId = await createSession(process.cwd());
|
|
210
|
+
const { text, tokens } = await sendMessage(currentSessionId, req.body.messages);
|
|
235
211
|
totalRequests++;
|
|
236
212
|
totalTokensUsed += tokens;
|
|
237
|
-
|
|
238
|
-
res.json({
|
|
239
|
-
id: `msg_${Date.now()}`,
|
|
240
|
-
type: "message",
|
|
241
|
-
role: "assistant",
|
|
242
|
-
content: [{ type: "text", text }],
|
|
243
|
-
model: currentModel,
|
|
244
|
-
stop_reason: "end_turn",
|
|
245
|
-
usage: {
|
|
246
|
-
input_tokens: Math.ceil((JSON.stringify(messages).length) / 4),
|
|
247
|
-
output_tokens: Math.ceil(text.length / 4)
|
|
248
|
-
}
|
|
249
|
-
});
|
|
213
|
+
res.json({ id: `msg_${Date.now()}`, type: "message", role: "assistant", content: [{ type: "text", text }], model: currentModel, stop_reason: "end_turn", usage: { input_tokens: Math.ceil(JSON.stringify(req.body.messages).length / 4), output_tokens: Math.ceil(text.length / 4) } });
|
|
250
214
|
} catch (error) {
|
|
251
|
-
console.error("Error:", error);
|
|
252
215
|
res.status(500).json({ error: { type: "api_error", message: error instanceof Error ? error.message : String(error) } });
|
|
253
216
|
}
|
|
254
217
|
});
|
|
255
218
|
|
|
256
219
|
app.post("/v1/messages/count_tokens", (req, res) => {
|
|
257
|
-
const { messages } = req.body;
|
|
258
220
|
let totalTokens = 0;
|
|
259
|
-
for (const msg of messages || [])
|
|
260
|
-
const content = extractTextFromContent(msg.content);
|
|
261
|
-
totalTokens += Math.ceil(content.length / 4);
|
|
262
|
-
}
|
|
221
|
+
for (const msg of req.body.messages || []) totalTokens += Math.ceil(extractTextFromContent(msg.content).length / 4);
|
|
263
222
|
res.json({ tokens: totalTokens });
|
|
264
223
|
});
|
|
265
224
|
|
|
266
|
-
|
|
225
|
+
const PORT = PROXY_PORT;
|
|
226
|
+
|
|
227
|
+
function generateHTML() {
|
|
267
228
|
return `<!DOCTYPE html>
|
|
268
229
|
<html lang="en">
|
|
269
230
|
<head>
|
|
270
231
|
<meta charset="UTF-8">
|
|
271
232
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
272
|
-
<title>OpenCode Bridge</title>
|
|
233
|
+
<title>OCB - OpenCode Bridge</title>
|
|
273
234
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
274
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
235
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
275
236
|
<style>
|
|
276
|
-
* {
|
|
277
|
-
body { font-family: 'Inter', -apple-system, sans-serif; background: #
|
|
278
|
-
|
|
279
|
-
.app-container { display: flex; height: 100vh; }
|
|
280
|
-
|
|
281
|
-
/* Sidebar */
|
|
282
|
-
.sidebar { width: 280px; background: #121218; border-right: 1px solid #27272a; display: flex; flex-direction: column; }
|
|
283
|
-
.sidebar-header { padding: 20px; border-bottom: 1px solid #27272a; }
|
|
284
|
-
.logo { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 16px; }
|
|
285
|
-
.logo-icon { width: 28px; height: 28px; background: linear-gradient(135deg, #6366f1, #8b5cf6); border-radius: 6px; display: flex; align-items: center; justify-content: center; }
|
|
286
|
-
|
|
287
|
-
/* Search */
|
|
288
|
-
.search-box { padding: 16px 20px; }
|
|
289
|
-
.search-input { width: 100%; padding: 10px 12px; background: #1a1a20; border: 1px solid #27272a; border-radius: 8px; color: #e4e4e7; font-size: 13px; outline: none; transition: border-color 0.2s; }
|
|
290
|
-
.search-input:focus { border-color: #6366f1; }
|
|
291
|
-
.search-input::placeholder { color: #71717a; }
|
|
292
|
-
|
|
293
|
-
/* Provider List */
|
|
294
|
-
.provider-list { flex: 1; overflow-y: auto; padding: 8px; }
|
|
295
|
-
.provider-item { padding: 10px 12px; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; font-size: 13px; transition: background 0.15s; }
|
|
296
|
-
.provider-item:hover { background: #1f1f26; }
|
|
297
|
-
.provider-item.active { background: #6366f1/15; color: #a5b4fc; }
|
|
298
|
-
.provider-name { display: flex; align-items: center; gap: 8px; }
|
|
299
|
-
.provider-count { font-size: 11px; color: #71717a; background: #27272a; padding: 2px 6px; border-radius: 4px; }
|
|
300
|
-
|
|
301
|
-
/* Main Content */
|
|
302
|
-
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
303
|
-
|
|
304
|
-
/* Header */
|
|
305
|
-
.main-header { padding: 16px 24px; border-bottom: 1px solid #27272a; display: flex; align-items: center; justify-content: space-between; background: #121218; }
|
|
306
|
-
.header-left { display: flex; align-items: center; gap: 16px; }
|
|
307
|
-
.current-model-badge { display: flex; align-items: center; gap: 8px; padding: 8px 14px; background: #6366f1/15; border: 1px solid #6366f1/30; border-radius: 8px; font-size: 13px; }
|
|
308
|
-
.status-dot { width: 8px; height: 8px; background: #22c55e; border-radius: 50%; }
|
|
309
|
-
.status-dot.inactive { background: #ef4444; }
|
|
310
|
-
|
|
311
|
-
/* Model List */
|
|
312
|
-
.model-list-container { flex: 1; overflow-y: auto; padding: 16px 24px; }
|
|
313
|
-
.model-list-header { font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: #71717a; margin-bottom: 12px; padding: 0 8px; }
|
|
314
|
-
.model-item { padding: 12px 16px; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; transition: all 0.15s; border: 1px solid transparent; margin-bottom: 4px; }
|
|
315
|
-
.model-item:hover { background: #1f1f26; }
|
|
316
|
-
.model-item.selected { background: #6366f1/15; border-color: #6366f1/40; }
|
|
317
|
-
.model-info { display: flex; flex-direction: column; gap: 2px; }
|
|
318
|
-
.model-name { font-size: 14px; font-weight: 500; }
|
|
319
|
-
.model-id { font-size: 11px; color: #71717a; font-family: 'JetBrains Mono', monospace; }
|
|
320
|
-
.model-cost { font-size: 11px; color: #a1a1aa; text-align: right; }
|
|
321
|
-
.model-cost span { display: block; }
|
|
322
|
-
|
|
323
|
-
/* Footer Stats */
|
|
324
|
-
.footer { padding: 12px 24px; border-top: 1px solid #27272a; display: flex; gap: 24px; background: #121218; }
|
|
325
|
-
.stat-item { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #a1a1aa; }
|
|
326
|
-
.stat-value { color: #e4e4e7; font-weight: 500; }
|
|
327
|
-
|
|
328
|
-
/* Scrollbar */
|
|
237
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
238
|
+
body { font-family: 'Inter', -apple-system, sans-serif; background: #09090b; color: #fafafa; min-height: 100vh; }
|
|
329
239
|
::-webkit-scrollbar { width: 6px; }
|
|
330
240
|
::-webkit-scrollbar-track { background: transparent; }
|
|
331
241
|
::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
|
|
332
|
-
::-webkit-scrollbar-thumb:hover { background: #52525b; }
|
|
333
|
-
|
|
334
|
-
/* Buttons */
|
|
335
|
-
.btn { padding: 8px 14px; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.15s; border: none; }
|
|
336
|
-
.btn-primary { background: #6366f1; color: white; }
|
|
337
|
-
.btn-primary:hover { background: #4f46e5; }
|
|
338
|
-
.btn-secondary { background: #27272a; color: #a1a1aa; }
|
|
339
|
-
.btn-secondary:hover { background: #3f3f46; color: #e4e4e7; }
|
|
340
242
|
</style>
|
|
341
243
|
</head>
|
|
342
244
|
<body>
|
|
343
|
-
<div class="
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
<div
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
</div>
|
|
352
|
-
|
|
353
|
-
<div class="search-box">
|
|
354
|
-
<input type="text" class="search-input" placeholder="Search models..." id="searchInput" oninput="filterModels()">
|
|
355
|
-
</div>
|
|
356
|
-
|
|
357
|
-
<div class="provider-list" id="providerList">
|
|
358
|
-
<!-- Providers loaded here -->
|
|
359
|
-
</div>
|
|
360
|
-
</div>
|
|
361
|
-
|
|
362
|
-
<!-- Main Content -->
|
|
363
|
-
<div class="main-content">
|
|
364
|
-
<div class="main-header">
|
|
365
|
-
<div class="header-left">
|
|
366
|
-
<div class="current-model-badge">
|
|
367
|
-
<div class="status-dot" id="statusDot"></div>
|
|
368
|
-
<span id="currentModelName">Loading...</span>
|
|
245
|
+
<div class="min-h-screen flex flex-col">
|
|
246
|
+
<header class="bg-zinc-900 border-b border-zinc-800 px-6 py-4">
|
|
247
|
+
<div class="max-w-7xl mx-auto flex items-center justify-between">
|
|
248
|
+
<div class="flex items-center gap-3">
|
|
249
|
+
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center text-xl font-bold">⚡</div>
|
|
250
|
+
<div>
|
|
251
|
+
<h1 class="text-xl font-semibold text-white">OCB</h1>
|
|
252
|
+
<p class="text-xs text-zinc-500">OpenCode Bridge</p>
|
|
369
253
|
</div>
|
|
370
254
|
</div>
|
|
371
|
-
<div
|
|
372
|
-
<
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
<div class="model-list-header" id="modelListHeader">All Models</div>
|
|
379
|
-
<div id="modelList">
|
|
380
|
-
<!-- Models loaded here -->
|
|
255
|
+
<div class="flex items-center gap-3">
|
|
256
|
+
<div class="flex items-center gap-2 px-3 py-1.5 bg-zinc-800 rounded-lg">
|
|
257
|
+
<div id="statusDot" class="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
258
|
+
<span id="currentModelDisplay" class="text-sm text-zinc-300">Loading...</span>
|
|
259
|
+
</div>
|
|
260
|
+
<button onclick="refreshModels()" class="px-3 py-1.5 text-sm bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition">↻</button>
|
|
261
|
+
<button onclick="resetSession()" class="px-3 py-1.5 text-sm bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg transition">Reset</button>
|
|
381
262
|
</div>
|
|
382
263
|
</div>
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
<
|
|
264
|
+
</header>
|
|
265
|
+
<main class="flex-1 flex max-w-7xl mx-auto w-full">
|
|
266
|
+
<aside class="w-64 bg-zinc-900 border-r border-zinc-800 flex flex-col">
|
|
267
|
+
<div class="p-4">
|
|
268
|
+
<input type="text" id="searchInput" placeholder="Search models..." oninput="filterData()" class="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-indigo-500">
|
|
388
269
|
</div>
|
|
389
|
-
<div class="
|
|
390
|
-
|
|
391
|
-
|
|
270
|
+
<div id="providerList" class="flex-1 overflow-y-auto px-2 pb-4"></div>
|
|
271
|
+
</aside>
|
|
272
|
+
<section class="flex-1 p-6 overflow-y-auto">
|
|
273
|
+
<div class="flex items-center justify-between mb-4">
|
|
274
|
+
<h2 id="sectionTitle" class="text-lg font-medium text-white">All Models</h2>
|
|
275
|
+
<span id="modelCount" class="text-sm text-zinc-500"></span>
|
|
392
276
|
</div>
|
|
393
|
-
<div class="
|
|
394
|
-
|
|
395
|
-
|
|
277
|
+
<div id="modelGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"></div>
|
|
278
|
+
</section>
|
|
279
|
+
</main>
|
|
280
|
+
<footer class="bg-zinc-900 border-t border-zinc-800 px-6 py-3">
|
|
281
|
+
<div class="max-w-7xl mx-auto flex items-center justify-between text-sm text-zinc-500">
|
|
282
|
+
<div class="flex items-center gap-6">
|
|
283
|
+
<span>Requests: <span id="totalRequests" class="text-white font-medium">0</span></span>
|
|
284
|
+
<span>Tokens: <span id="totalTokens" class="text-white font-medium">0</span></span>
|
|
396
285
|
</div>
|
|
286
|
+
<span id="totalModelsDisplay">Loading...</span>
|
|
397
287
|
</div>
|
|
398
|
-
</
|
|
288
|
+
</footer>
|
|
399
289
|
</div>
|
|
400
|
-
|
|
401
290
|
<script>
|
|
402
|
-
const PROXY_PORT = ${PROXY_PORT};
|
|
403
291
|
let allModels = [];
|
|
404
292
|
let groupedModels = {};
|
|
405
293
|
let currentProvider = 'all';
|
|
406
294
|
let currentModelId = null;
|
|
407
|
-
|
|
295
|
+
|
|
408
296
|
async function loadModels() {
|
|
409
297
|
const res = await fetch('/api/models');
|
|
410
298
|
const data = await res.json();
|
|
411
|
-
|
|
412
299
|
allModels = data.models || [];
|
|
413
300
|
groupedModels = data.grouped || {};
|
|
414
|
-
|
|
415
301
|
renderProviders();
|
|
416
302
|
renderModels(allModels);
|
|
417
303
|
updateStats();
|
|
418
304
|
}
|
|
419
|
-
|
|
305
|
+
|
|
420
306
|
function renderProviders() {
|
|
421
307
|
const container = document.getElementById('providerList');
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
</div
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
html += \`<div class="provider-item \${currentProvider === provider ? 'active' : ''}" onclick="selectProvider('\${provider}')">
|
|
431
|
-
<span class="provider-name">📦 \${provider}</span>
|
|
432
|
-
<span class="provider-count">\${models.length}</span>
|
|
433
|
-
</div>\`;
|
|
308
|
+
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
309
|
+
let providersToShow = Object.entries(groupedModels);
|
|
310
|
+
if (searchTerm) {
|
|
311
|
+
providersToShow = providersToShow.filter(([name, models]) => name.toLowerCase().includes(searchTerm) || models.some(m => m.name.toLowerCase().includes(searchTerm)));
|
|
312
|
+
}
|
|
313
|
+
let html = '<div onclick="selectProvider('all')" class="px-3 py-2 rounded-lg cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === 'all' ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span>🏠 All</span><span class="text-xs opacity-60">' + allModels.length + '</span></div>';
|
|
314
|
+
for (const [provider, models] of providersToShow) {
|
|
315
|
+
html += '<div onclick="selectProvider(\\'' + provider + '\\')" class="px-3 py-2 rounded-lg cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === provider ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span class="truncate">' + provider + '</span><span class="text-xs opacity-60">' + models.length + '</span></div>';
|
|
434
316
|
}
|
|
435
|
-
|
|
436
317
|
container.innerHTML = html;
|
|
437
318
|
}
|
|
438
|
-
|
|
319
|
+
|
|
439
320
|
function selectProvider(provider) {
|
|
440
321
|
currentProvider = provider;
|
|
322
|
+
document.getElementById('sectionTitle').textContent = provider === 'all' ? 'All Models' : provider;
|
|
441
323
|
renderProviders();
|
|
442
|
-
|
|
443
|
-
const models = provider === 'all' ? allModels : groupedModels[provider] || [];
|
|
444
|
-
document.getElementById('modelListHeader').textContent = provider === 'all' ? 'All Models' : provider;
|
|
445
|
-
renderModels(models);
|
|
324
|
+
renderModels(provider === 'all' ? allModels : (groupedModels[provider] || []));
|
|
446
325
|
}
|
|
447
|
-
|
|
326
|
+
|
|
448
327
|
function renderModels(models) {
|
|
449
|
-
const container = document.getElementById('
|
|
328
|
+
const container = document.getElementById('modelGrid');
|
|
450
329
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
451
|
-
|
|
452
330
|
let filtered = models;
|
|
453
331
|
if (searchTerm) {
|
|
454
|
-
filtered = models.filter(m =>
|
|
455
|
-
m.name.toLowerCase().includes(searchTerm) ||
|
|
456
|
-
m.id.toLowerCase().includes(searchTerm) ||
|
|
457
|
-
m.provider.toLowerCase().includes(searchTerm)
|
|
458
|
-
);
|
|
332
|
+
filtered = models.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
|
|
459
333
|
}
|
|
460
|
-
|
|
334
|
+
document.getElementById('modelCount').textContent = filtered.length + ' models';
|
|
461
335
|
if (filtered.length === 0) {
|
|
462
|
-
container.innerHTML = '<div
|
|
336
|
+
container.innerHTML = '<div class="col-span-full text-center text-zinc-500 py-12">No models found</div>';
|
|
463
337
|
return;
|
|
464
338
|
}
|
|
465
|
-
|
|
466
|
-
let html = '';
|
|
467
|
-
for (const model of filtered) {
|
|
468
|
-
const isSelected = model.id === currentModelId;
|
|
469
|
-
const costInfo = model.cost ? \`<span>$\${model.cost.input}/M in</span><span>$\${model.cost.output}/M out</span>\` : '';
|
|
470
|
-
|
|
471
|
-
html += \`<div class="model-item \${isSelected ? 'selected' : ''}" onclick="selectModel('\${model.id}')">
|
|
472
|
-
<div class="model-info">
|
|
473
|
-
<div class="model-name">\${model.name}</div>
|
|
474
|
-
<div class="model-id">\${model.id}</div>
|
|
475
|
-
</div>
|
|
476
|
-
<div class="model-cost">\${costInfo}</div>
|
|
477
|
-
</div>\`;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
container.innerHTML = html;
|
|
339
|
+
container.innerHTML = filtered.map(m => '<div onclick="selectModel(\\'' + m.id + '\\')" class="p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02] ' + (m.id === currentModelId ? 'bg-indigo-600/20 border-indigo-500' : 'bg-zinc-900 border-zinc-800 hover:border-zinc-700') + '"><div class="flex items-start justify-between mb-2"><h3 class="font-medium text-white truncate">' + m.name + '</h3>' + (m.id === currentModelId ? '<span class="text-xs bg-indigo-500 text-white px-2 py-0.5 rounded">Active</span>' : '') + '</div><p class="text-xs text-zinc-500 font-mono truncate mb-2">' + m.id + '</p>' + (m.cost ? '<p class="text-xs text-zinc-400">$' + m.cost.input + '/M → $' + m.cost.output + '/M</p>' : '') + '</div>').join('');
|
|
481
340
|
}
|
|
482
|
-
|
|
483
|
-
function
|
|
484
|
-
const
|
|
485
|
-
|
|
341
|
+
|
|
342
|
+
function filterData() {
|
|
343
|
+
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
344
|
+
if (searchTerm) {
|
|
345
|
+
const filtered = allModels.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
|
|
346
|
+
document.getElementById('sectionTitle').textContent = 'Search Results';
|
|
347
|
+
renderModels(filtered);
|
|
348
|
+
renderProviders();
|
|
349
|
+
} else {
|
|
350
|
+
selectProvider(currentProvider);
|
|
351
|
+
}
|
|
486
352
|
}
|
|
487
|
-
|
|
353
|
+
|
|
488
354
|
async function selectModel(modelId) {
|
|
489
|
-
const res = await fetch('/api/model', {
|
|
490
|
-
method: 'POST',
|
|
491
|
-
headers: { 'Content-Type': 'application/json' },
|
|
492
|
-
body: JSON.stringify({ modelId })
|
|
493
|
-
});
|
|
355
|
+
const res = await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
|
|
494
356
|
const data = await res.json();
|
|
495
357
|
if (data.success) {
|
|
496
358
|
currentModelId = modelId;
|
|
497
359
|
loadStatus();
|
|
498
|
-
|
|
499
|
-
renderModels(models);
|
|
360
|
+
renderModels(currentProvider === 'all' ? allModels : (groupedModels[currentProvider] || []));
|
|
500
361
|
}
|
|
501
362
|
}
|
|
502
|
-
|
|
363
|
+
|
|
503
364
|
async function loadStatus() {
|
|
504
365
|
const res = await fetch('/api/status');
|
|
505
366
|
const data = await res.json();
|
|
506
|
-
|
|
507
367
|
const model = allModels.find(m => m.id === data.currentModel);
|
|
508
|
-
document.getElementById('
|
|
509
|
-
|
|
510
|
-
const statusDot = document.getElementById('statusDot');
|
|
511
|
-
if (data.sessionId === 'active') {
|
|
512
|
-
statusDot.classList.remove('inactive');
|
|
513
|
-
} else {
|
|
514
|
-
statusDot.classList.add('inactive');
|
|
515
|
-
}
|
|
516
|
-
|
|
368
|
+
document.getElementById('currentModelDisplay').textContent = model?.name || data.currentModel;
|
|
369
|
+
document.getElementById('statusDot').className = 'w-2 h-2 rounded-full ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
|
|
517
370
|
document.getElementById('totalRequests').textContent = data.totalRequests.toLocaleString();
|
|
518
371
|
document.getElementById('totalTokens').textContent = data.totalTokensUsed.toLocaleString();
|
|
519
|
-
document.getElementById('
|
|
520
|
-
|
|
372
|
+
document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
|
|
521
373
|
currentModelId = data.currentModel;
|
|
522
374
|
}
|
|
523
|
-
|
|
375
|
+
|
|
524
376
|
async function resetSession() {
|
|
525
377
|
await fetch('/api/reset-session', { method: 'POST' });
|
|
526
378
|
loadStatus();
|
|
527
379
|
}
|
|
528
|
-
|
|
529
|
-
async function
|
|
530
|
-
await fetch('/api/
|
|
531
|
-
|
|
380
|
+
|
|
381
|
+
async function refreshModels() {
|
|
382
|
+
await fetch('/api/refresh-models', { method: 'POST' });
|
|
383
|
+
loadModels();
|
|
532
384
|
}
|
|
533
|
-
|
|
534
|
-
// Initialize
|
|
385
|
+
|
|
535
386
|
loadModels();
|
|
536
387
|
loadStatus();
|
|
537
388
|
setInterval(loadStatus, 3000);
|
|
@@ -540,7 +391,7 @@ function generateHTML(): string {
|
|
|
540
391
|
</html>`;
|
|
541
392
|
}
|
|
542
393
|
|
|
543
|
-
app.listen(
|
|
544
|
-
console.error(`OpenCode Bridge running on http://localhost:${
|
|
545
|
-
console.error(`Dashboard: http://localhost:${
|
|
394
|
+
app.listen(PORT, () => {
|
|
395
|
+
console.error(`OpenCode Bridge running on http://localhost:${PORT}`);
|
|
396
|
+
console.error(`Dashboard: http://localhost:${PORT}`);
|
|
546
397
|
});
|