@oooooooooo/ch0de 1.9.57 → 1.9.58
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/chode.js +85 -2
- package/package.json +1 -1
- package/serve-frontend.html +118 -3
package/chode.js
CHANGED
|
@@ -7298,6 +7298,40 @@ function cmdServe(argv) {
|
|
|
7298
7298
|
return 'ses_' + crypto.randomBytes(8).toString('hex');
|
|
7299
7299
|
}
|
|
7300
7300
|
|
|
7301
|
+
function verifyProviderKey(baseUrl, key) {
|
|
7302
|
+
return new Promise(function(resolve){
|
|
7303
|
+
try {
|
|
7304
|
+
var u = new URL(baseUrl + '/models');
|
|
7305
|
+
var req = http.request(u, {
|
|
7306
|
+
method: 'GET',
|
|
7307
|
+
headers: {'Authorization': 'Bearer ' + key, 'Accept': 'application/json'},
|
|
7308
|
+
timeout: 8000
|
|
7309
|
+
}, function(resp){
|
|
7310
|
+
var working = resp.statusCode >= 200 && resp.statusCode < 400;
|
|
7311
|
+
resp.resume();
|
|
7312
|
+
resolve(working);
|
|
7313
|
+
});
|
|
7314
|
+
req.on('error', function(){ resolve(false); });
|
|
7315
|
+
req.on('timeout', function(){ req.destroy(); resolve(false); });
|
|
7316
|
+
req.end();
|
|
7317
|
+
} catch(e) { resolve(false); }
|
|
7318
|
+
});
|
|
7319
|
+
}
|
|
7320
|
+
|
|
7321
|
+
function providerInfo(pid) {
|
|
7322
|
+
var info = null;
|
|
7323
|
+
try {
|
|
7324
|
+
for (var id in PROVIDERS) { if (id === pid) { info = PROVIDERS[id]; break; } }
|
|
7325
|
+
} catch(e) {}
|
|
7326
|
+
if (info) return info;
|
|
7327
|
+
try {
|
|
7328
|
+
var dir = require('./provider-directory');
|
|
7329
|
+
var listed = dir.list();
|
|
7330
|
+
for (var i = 0; i < listed.length; i++) { if (listed[i].id === pid) { info = listed[i]; break; } }
|
|
7331
|
+
} catch(e) {}
|
|
7332
|
+
return info;
|
|
7333
|
+
}
|
|
7334
|
+
|
|
7301
7335
|
function loadServeConfig() {
|
|
7302
7336
|
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch(e) { return { freeOnly: true, defaultProvider: 'auto', providers: {}, connections: {} }; }
|
|
7303
7337
|
}
|
|
@@ -7434,9 +7468,11 @@ function cmdServe(argv) {
|
|
|
7434
7468
|
if (pathname === '/api/providers' && req.method === 'GET') {
|
|
7435
7469
|
var out = [];
|
|
7436
7470
|
try {
|
|
7471
|
+
var cfg = loadServeConfig();
|
|
7472
|
+
var keys = cfg.providerKeys || {};
|
|
7437
7473
|
for (var id in PROVIDERS) {
|
|
7438
7474
|
var p = PROVIDERS[id];
|
|
7439
|
-
out.push({id: id, name: p.name || id, requiresKey: !!p.requiresKey, free: require('./provider-capacity').free ? require('./provider-capacity').free(p) : true, category: p.category || '', builtIn: true});
|
|
7475
|
+
out.push({id: id, name: p.name || id, requiresKey: !!p.requiresKey, free: require('./provider-capacity').free ? require('./provider-capacity').free(p) : true, category: p.category || '', builtIn: true, hasKey: !!keys[id]});
|
|
7440
7476
|
}
|
|
7441
7477
|
var dir = require('./provider-directory');
|
|
7442
7478
|
var listed = dir.list();
|
|
@@ -7444,13 +7480,60 @@ function cmdServe(argv) {
|
|
|
7444
7480
|
out.forEach(function(x){ids[x.id]=true;});
|
|
7445
7481
|
listed.forEach(function(p){
|
|
7446
7482
|
if (!ids[p.id]) {
|
|
7447
|
-
out.push({id: p.id, name: p.name, requiresKey: !!p.env, free: !p.env, category: 'directory', builtIn: false, baseUrl: p.baseUrl, models: p.models.length, doc: p.doc, verification: p.verification});
|
|
7483
|
+
out.push({id: p.id, name: p.name, requiresKey: !!p.env, free: !p.env, category: 'directory', builtIn: false, baseUrl: p.baseUrl, models: p.models.length, doc: p.doc, verification: p.verification, hasKey: !!keys[p.id]});
|
|
7448
7484
|
}
|
|
7449
7485
|
});
|
|
7450
7486
|
} catch(e) {}
|
|
7451
7487
|
json(res, 200, {ok: true, providers: out});
|
|
7452
7488
|
}
|
|
7453
7489
|
|
|
7490
|
+
var keyM = pathname.match(/^\/api\/provider\/([^\/]+)\/key(\/test)?$/);
|
|
7491
|
+
if (keyM) {
|
|
7492
|
+
var pid = keyM[1];
|
|
7493
|
+
var isTest = !!keyM[2];
|
|
7494
|
+
if (req.method === 'GET' && !isTest) {
|
|
7495
|
+
var cfg2 = loadServeConfig();
|
|
7496
|
+
var keys2 = cfg2.providerKeys || {};
|
|
7497
|
+
json(res, 200, {ok: true, hasKey: !!keys2[pid]});
|
|
7498
|
+
return;
|
|
7499
|
+
}
|
|
7500
|
+
if (req.method === 'POST' && !isTest) {
|
|
7501
|
+
readBody(req).then(function(body){
|
|
7502
|
+
var parsed;
|
|
7503
|
+
try { parsed = JSON.parse(body || '{}'); } catch(e) { json(res, 400, {ok:false, error:'invalid json'}); return; }
|
|
7504
|
+
var key = String(parsed.key || '');
|
|
7505
|
+
var cfg3 = loadServeConfig();
|
|
7506
|
+
if (!cfg3.providerKeys) cfg3.providerKeys = {};
|
|
7507
|
+
cfg3.providerKeys[pid] = key;
|
|
7508
|
+
saveServeConfig(cfg3);
|
|
7509
|
+
var pinfo = providerInfo(pid);
|
|
7510
|
+
if (pinfo && pinfo.baseUrl) {
|
|
7511
|
+
verifyProviderKey(pinfo.baseUrl, key).then(function(working){
|
|
7512
|
+
json(res, 200, {ok: true, working: working});
|
|
7513
|
+
}).catch(function(){ json(res, 200, {ok: true, working: false}); });
|
|
7514
|
+
} else {
|
|
7515
|
+
json(res, 200, {ok: true, working: false});
|
|
7516
|
+
}
|
|
7517
|
+
}).catch(function(e){ json(res, 500, {ok:false, error: e.message}); });
|
|
7518
|
+
return;
|
|
7519
|
+
}
|
|
7520
|
+
if (req.method === 'GET' && isTest) {
|
|
7521
|
+
var cfg4 = loadServeConfig();
|
|
7522
|
+
var keys4 = cfg4.providerKeys || {};
|
|
7523
|
+
var key = String(keys4[pid] || '');
|
|
7524
|
+
if (!key) { json(res, 200, {ok: true, working: false, hasKey: false}); return; }
|
|
7525
|
+
var pinfo2 = providerInfo(pid);
|
|
7526
|
+
if (pinfo2 && pinfo2.baseUrl) {
|
|
7527
|
+
verifyProviderKey(pinfo2.baseUrl, key).then(function(working){
|
|
7528
|
+
json(res, 200, {ok: true, working: working, hasKey: true});
|
|
7529
|
+
}).catch(function(){ json(res, 200, {ok: true, working: false, hasKey: true}); });
|
|
7530
|
+
} else {
|
|
7531
|
+
json(res, 200, {ok: true, working: false, hasKey: true});
|
|
7532
|
+
}
|
|
7533
|
+
return;
|
|
7534
|
+
}
|
|
7535
|
+
}
|
|
7536
|
+
|
|
7454
7537
|
res.writeHead(404, {'Content-Type': 'text/plain'});
|
|
7455
7538
|
res.end('Not found');
|
|
7456
7539
|
});
|
package/package.json
CHANGED
package/serve-frontend.html
CHANGED
|
@@ -58,8 +58,9 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
|
|
|
58
58
|
.setting-select, .setting-input { background: #000; border: 1px solid #22dd5533; color: #e0e0e0; padding: 6px 10px; border-radius: 6px; font-family: inherit; font-size: 12px; outline: none; }
|
|
59
59
|
.setting-select:focus, .setting-input:focus { border-color: #22dd55; }
|
|
60
60
|
.setting-checkbox { width: 18px; height: 18px; accent-color: #22dd55; background: #000; border: 1px solid #22dd5533; cursor: pointer; }
|
|
61
|
-
.provider-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #141414; font-size: 12px; }
|
|
61
|
+
.provider-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #141414; font-size: 12px; cursor: pointer; }
|
|
62
62
|
.provider-row:last-child { border-bottom: none; }
|
|
63
|
+
.provider-row:hover { background: #141414; }
|
|
63
64
|
#providersList { max-height: 340px; overflow-y: auto; border: 1px solid #141414; border-radius: 6px; padding: 10px; background: #000; margin-top: 8px; }
|
|
64
65
|
.provider-info { display: flex; align-items: center; gap: 10px; flex: 1; }
|
|
65
66
|
.provider-name { font-weight: bold; color: #e0e0e0; }
|
|
@@ -67,9 +68,27 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
|
|
|
67
68
|
.provider-status { font-size: 11px; padding: 2px 8px; border-radius: 4px; }
|
|
68
69
|
.provider-status.free { background: #1a1a1a; color: #22dd55; border: 1px solid #22dd5533; }
|
|
69
70
|
.provider-status.keyed { background: #1a1a1a; color: #ff9900; border: 1px solid #ff990033; }
|
|
71
|
+
.provider-status.haskey { background: #1a1a1a; color: #22dd55; border: 1px solid #22dd5533; }
|
|
72
|
+
.provider-status.working { background: #1a1a1a; color: #22dd55; border: 1px solid #22dd55; }
|
|
73
|
+
.provider-status.badkey { background: #1a1a1a; color: #ff4444; border: 1px solid #ff444433; }
|
|
70
74
|
.btn-save { background: #22dd55; color: #000; border: none; border-radius: 6px; padding: 8px 16px; font-family: inherit; font-weight: bold; cursor: pointer; margin-top: 20px; }
|
|
71
75
|
.btn-save:hover { background: #3dffa0; }
|
|
72
76
|
.btn-save:disabled { background: #1a1a1a; color: #666; cursor: not-allowed; }
|
|
77
|
+
|
|
78
|
+
/* Key input modal */
|
|
79
|
+
.key-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 100; display: none; }
|
|
80
|
+
.key-modal { position: fixed; top: 20%; left: 25%; right: 25%; background: #0a0a0a; border: 1px solid #22dd5533; border-radius: 12px; z-index: 110; padding: 24px; display: none; box-shadow: 0 4px 20px rgba(0,0,0,0.4); }
|
|
81
|
+
.key-modal.open, .key-modal-overlay.open { display: block; }
|
|
82
|
+
.key-modal-title { color: #22dd55; font-weight: bold; font-size: 1.1rem; margin-bottom: 16px; }
|
|
83
|
+
.key-modal-field { margin-bottom: 16px; }
|
|
84
|
+
.key-modal-label { display: block; color: #888; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
|
|
85
|
+
.key-modal-input { width: 100%; background: #000; border: 1px solid #22dd5533; color: #e0e0e0; padding: 10px 14px; border-radius: 6px; font-family: inherit; font-size: 13px; outline: none; }
|
|
86
|
+
.key-modal-input:focus { border-color: #22dd55; }
|
|
87
|
+
.key-modal-actions { display: flex; gap: 10px; margin-top: 20px; }
|
|
88
|
+
.key-status { margin-top: 12px; padding: 8px 12px; border-radius: 6px; font-size: 12px; display: none; }
|
|
89
|
+
.key-status.ok { background: #141414; color: #22dd55; border: 1px solid #22dd5533; display: block; }
|
|
90
|
+
.key-status.err { background: #141414; color: #ff4444; border: 1px solid #ff444433; display: block; }
|
|
91
|
+
.key-status.saving { background: #141414; color: #ff9900; border: 1px solid #ff990033; display: block; }
|
|
73
92
|
</style>
|
|
74
93
|
</head>
|
|
75
94
|
<body>
|
|
@@ -175,6 +194,19 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
|
|
|
175
194
|
</div>
|
|
176
195
|
</div>
|
|
177
196
|
</div>
|
|
197
|
+
<div class="key-modal-overlay" id="keyModalOverlay"></div>
|
|
198
|
+
<div class="key-modal" id="keyModal">
|
|
199
|
+
<div class="key-modal-title" id="keyModalTitle">API Key</div>
|
|
200
|
+
<div class="key-modal-field">
|
|
201
|
+
<span class="key-modal-label" id="keyModalLabel">API Key</span>
|
|
202
|
+
<input type="password" id="keyModalInput" class="key-modal-input" placeholder="Enter API key">
|
|
203
|
+
</div>
|
|
204
|
+
<div class="key-modal-actions">
|
|
205
|
+
<button class="btn-save" id="keyCancelBtn" style="background:#141414;color:#e0e0e0;border:1px solid #22dd5533;">Cancel</button>
|
|
206
|
+
<button class="btn-save" id="keySubmitBtn">Save & Test</button>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="key-status" id="keyStatus"></div>
|
|
209
|
+
</div>
|
|
178
210
|
<script>
|
|
179
211
|
const API = "http://127.0.0.1:" + (window.location.port || "4096");
|
|
180
212
|
let currentSessionId = null;
|
|
@@ -309,8 +341,10 @@ async function loadProviders() {
|
|
|
309
341
|
select.appendChild(opt);
|
|
310
342
|
var row = document.createElement("div");
|
|
311
343
|
row.className = "provider-row";
|
|
312
|
-
var statusCls
|
|
313
|
-
|
|
344
|
+
var statusCls, statusText;
|
|
345
|
+
if (!p.requiresKey) { statusCls = "free"; statusText = "free"; }
|
|
346
|
+
else if (p.hasKey) { statusCls = "working"; statusText = "key saved"; }
|
|
347
|
+
else { statusCls = "keyed"; statusText = "needs API key"; }
|
|
314
348
|
var extra = p.builtIn ? "" : ' <span style="font-size:10px;color:#888;margin-left:6px;">(' + (p.models||0) + ' models)</span>';
|
|
315
349
|
row.innerHTML =
|
|
316
350
|
'<div class="provider-info">' +
|
|
@@ -318,6 +352,7 @@ async function loadProviders() {
|
|
|
318
352
|
'<div class="provider-id">' + escapeHtml(p.id) + (p.baseUrl ? ' · ' + escapeHtml(String(p.baseUrl).slice(0,40)) : '') + '</div>' +
|
|
319
353
|
'</div>' +
|
|
320
354
|
'<div class="provider-status ' + statusCls + '">' + statusText + '</div>';
|
|
355
|
+
row.onclick = function() { openKeyModal(p); };
|
|
321
356
|
list.appendChild(row);
|
|
322
357
|
});
|
|
323
358
|
} catch(e) {}
|
|
@@ -366,6 +401,80 @@ function closeSettings() {
|
|
|
366
401
|
settingsOpen = false;
|
|
367
402
|
}
|
|
368
403
|
|
|
404
|
+
var keyModalProvider = null;
|
|
405
|
+
function openKeyModal(p) {
|
|
406
|
+
keyModalProvider = p;
|
|
407
|
+
document.getElementById("keyModalTitle").textContent = "API Key · " + (p.name || p.id);
|
|
408
|
+
document.getElementById("keyModalLabel").textContent = p.env ? ("API Key (" + p.env + ")") : "API Key";
|
|
409
|
+
document.getElementById("keyModalInput").value = "";
|
|
410
|
+
document.getElementById("keyStatus").style.display = "none";
|
|
411
|
+
document.getElementById("keyModalOverlay").classList.add("open");
|
|
412
|
+
document.getElementById("keyModal").classList.add("open");
|
|
413
|
+
document.getElementById("keyModalInput").focus();
|
|
414
|
+
}
|
|
415
|
+
function closeKeyModal() {
|
|
416
|
+
document.getElementById("keyModalOverlay").classList.remove("open");
|
|
417
|
+
document.getElementById("keyModal").classList.remove("open");
|
|
418
|
+
keyModalProvider = null;
|
|
419
|
+
}
|
|
420
|
+
async function submitKey() {
|
|
421
|
+
if (!keyModalProvider) return;
|
|
422
|
+
var input = document.getElementById("keyModalInput");
|
|
423
|
+
var key = input.value.trim();
|
|
424
|
+
if (!key) return;
|
|
425
|
+
var status = document.getElementById("keyStatus");
|
|
426
|
+
var btn = document.getElementById("keySubmitBtn");
|
|
427
|
+
status.textContent = "Saving and testing key…";
|
|
428
|
+
status.className = "key-status saving";
|
|
429
|
+
status.style.display = "block";
|
|
430
|
+
btn.disabled = true;
|
|
431
|
+
try {
|
|
432
|
+
const res = await fetch(API + "/api/provider/" + encodeURIComponent(keyModalProvider.id) + "/key", {
|
|
433
|
+
method: "POST",
|
|
434
|
+
headers: { "Content-Type": "application/json" },
|
|
435
|
+
body: JSON.stringify({ key: key })
|
|
436
|
+
});
|
|
437
|
+
const data = await res.json();
|
|
438
|
+
if (data.ok && data.working) {
|
|
439
|
+
status.textContent = "Key is working ✓";
|
|
440
|
+
status.className = "key-status ok";
|
|
441
|
+
// Refresh provider list to show updated status
|
|
442
|
+
await loadProviders();
|
|
443
|
+
setTimeout(closeKeyModal, 800);
|
|
444
|
+
} else {
|
|
445
|
+
status.textContent = "Key saved but could not be verified";
|
|
446
|
+
status.className = "key-status err";
|
|
447
|
+
}
|
|
448
|
+
} catch(e) {
|
|
449
|
+
status.textContent = "Error: " + e.message;
|
|
450
|
+
status.className = "key-status err";
|
|
451
|
+
} finally {
|
|
452
|
+
btn.disabled = false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async function testKey() {
|
|
456
|
+
if (!keyModalProvider) return;
|
|
457
|
+
var status = document.getElementById("keyStatus");
|
|
458
|
+
status.textContent = "Testing key…";
|
|
459
|
+
status.className = "key-status saving";
|
|
460
|
+
status.style.display = "block";
|
|
461
|
+
try {
|
|
462
|
+
const res = await fetch(API + "/api/provider/" + encodeURIComponent(keyModalProvider.id) + "/key/test");
|
|
463
|
+
const data = await res.json();
|
|
464
|
+
if (data.ok && data.working) {
|
|
465
|
+
status.textContent = "Key is working ✓";
|
|
466
|
+
status.className = "key-status ok";
|
|
467
|
+
await loadProviders();
|
|
468
|
+
} else {
|
|
469
|
+
status.textContent = "Key verification failed";
|
|
470
|
+
status.className = "key-status err";
|
|
471
|
+
}
|
|
472
|
+
} catch(e) {
|
|
473
|
+
status.textContent = "Error: " + e.message;
|
|
474
|
+
status.className = "key-status err";
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
369
478
|
document.getElementById("newSessionBtn").onclick = createSession;
|
|
370
479
|
document.getElementById("sendBtn").onclick = sendMessage;
|
|
371
480
|
document.getElementById("messageInput").addEventListener("keypress", function(e) {
|
|
@@ -375,6 +484,12 @@ document.getElementById("settingsBtn").onclick = openSettings;
|
|
|
375
484
|
document.getElementById("settingsClose").onclick = closeSettings;
|
|
376
485
|
document.getElementById("settingsOverlay").onclick = function(e) { if (e.target === this) closeSettings(); };
|
|
377
486
|
document.getElementById("settingsSave").onclick = function() { saveSettings(); closeSettings(); };
|
|
487
|
+
document.getElementById("keyCancelBtn").onclick = closeKeyModal;
|
|
488
|
+
document.getElementById("keySubmitBtn").onclick = submitKey;
|
|
489
|
+
document.getElementById("keyModalOverlay").onclick = function(e) { if (e.target === this) closeKeyModal(); };
|
|
490
|
+
document.getElementById("keyModalInput").addEventListener("keypress", function(e) {
|
|
491
|
+
if (e.key === "Enter") submitKey();
|
|
492
|
+
});
|
|
378
493
|
|
|
379
494
|
document.querySelectorAll(".settings-tab").forEach(function(tab) {
|
|
380
495
|
tab.onclick = function() {
|