@jacobbd/relay-ai 0.9.2 → 0.9.4
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/{chunk-GQCFLSEM.js → chunk-KDIY732Q.js} +510 -186
- package/dist/chunk-KDIY732Q.js.map +1 -0
- package/dist/{chunk-Q2FTCICO.js → chunk-NYKVDBQC.js} +2 -2
- package/dist/{chunk-Q2FTCICO.js.map → chunk-NYKVDBQC.js.map} +1 -1
- package/dist/cli.js +148 -9
- package/dist/cli.js.map +1 -1
- package/dist/core/index.d.ts +32 -4
- package/dist/core/index.js +617 -109
- package/dist/core/index.js.map +1 -1
- package/dist/{provider-templates-XKNRKAQU.js → provider-templates-CGWE66TD.js} +2 -2
- package/dist/ui/public/app.js +190 -1
- package/dist/{ui-command-SDMYDYT6.js → ui-command-OIY4243G.js} +59 -7
- package/dist/ui-command-OIY4243G.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-GQCFLSEM.js.map +0 -1
- package/dist/ui-command-SDMYDYT6.js.map +0 -1
- /package/dist/{provider-templates-XKNRKAQU.js.map → provider-templates-CGWE66TD.js.map} +0 -0
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
listAddableTemplates,
|
|
8
8
|
listSupportedTemplates,
|
|
9
9
|
listVisibleOAuthTemplates
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-NYKVDBQC.js";
|
|
11
11
|
init_provider_templates();
|
|
12
12
|
export {
|
|
13
13
|
PROVIDER_TEMPLATES,
|
|
@@ -17,4 +17,4 @@ export {
|
|
|
17
17
|
listSupportedTemplates,
|
|
18
18
|
listVisibleOAuthTemplates
|
|
19
19
|
};
|
|
20
|
-
//# sourceMappingURL=provider-templates-
|
|
20
|
+
//# sourceMappingURL=provider-templates-CGWE66TD.js.map
|
package/dist/ui/public/app.js
CHANGED
|
@@ -884,7 +884,23 @@ function buildCustomEndpointBodyContent(template, card) {
|
|
|
884
884
|
feedback.textContent = 'Connecting and fetching models…';
|
|
885
885
|
feedback.className = 'key-feedback muted';
|
|
886
886
|
|
|
887
|
-
|
|
887
|
+
let result = await api('POST', '/api/providers/add-custom', { kind, displayName, baseUrl, apiKey, headers });
|
|
888
|
+
|
|
889
|
+
if (!result.ok && result.duplicateOf) {
|
|
890
|
+
const proceed = window.confirm(
|
|
891
|
+
`You already have a backend with the same URL, key and headers (${result.duplicateOf}).\n\nAdd another anyway?`,
|
|
892
|
+
);
|
|
893
|
+
if (!proceed) {
|
|
894
|
+
addBtn.disabled = false;
|
|
895
|
+
feedback.textContent = 'Cancelled.';
|
|
896
|
+
feedback.className = 'key-feedback muted';
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
feedback.textContent = 'Connecting and fetching models…';
|
|
900
|
+
result = await api('POST', '/api/providers/add-custom', {
|
|
901
|
+
kind, displayName, baseUrl, apiKey, headers, confirmDuplicate: true,
|
|
902
|
+
});
|
|
903
|
+
}
|
|
888
904
|
|
|
889
905
|
addBtn.disabled = false;
|
|
890
906
|
if (result.ok) {
|
|
@@ -1367,10 +1383,183 @@ function buildProviderBodyContent(provider) {
|
|
|
1367
1383
|
setTimeout(() => { feedback.textContent = ''; feedback.className = 'key-feedback'; }, 4000);
|
|
1368
1384
|
});
|
|
1369
1385
|
|
|
1386
|
+
if (provider.customEndpoint) content.appendChild(buildEditCustomEndpointRow(provider));
|
|
1370
1387
|
content.appendChild(buildDeleteProviderRow(provider));
|
|
1371
1388
|
return content;
|
|
1372
1389
|
}
|
|
1373
1390
|
|
|
1391
|
+
function buildEditCustomEndpointRow(provider) {
|
|
1392
|
+
const custom = provider.customEndpoint;
|
|
1393
|
+
const wrapper = document.createElement('div');
|
|
1394
|
+
wrapper.style.cssText = 'margin-top:12px;padding-top:12px;border-top:1px solid oklch(22% 0.015 265)';
|
|
1395
|
+
|
|
1396
|
+
const editBtn = document.createElement('button');
|
|
1397
|
+
editBtn.className = 'btn btn-ghost';
|
|
1398
|
+
editBtn.textContent = 'Edit Backend Settings';
|
|
1399
|
+
|
|
1400
|
+
const form = document.createElement('div');
|
|
1401
|
+
form.style.cssText = 'display:none;flex-direction:column;gap:8px;margin-top:10px';
|
|
1402
|
+
|
|
1403
|
+
function row(label, input) {
|
|
1404
|
+
const wrap = document.createElement('div');
|
|
1405
|
+
wrap.className = 'key-row';
|
|
1406
|
+
wrap.style.flexDirection = 'column';
|
|
1407
|
+
wrap.style.gap = '4px';
|
|
1408
|
+
const lbl = document.createElement('label');
|
|
1409
|
+
lbl.textContent = label;
|
|
1410
|
+
lbl.style.cssText = 'font-size:12px;color:var(--text-muted,#aaa);display:block';
|
|
1411
|
+
wrap.appendChild(lbl);
|
|
1412
|
+
wrap.appendChild(input);
|
|
1413
|
+
return wrap;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
const nameInput = document.createElement('input');
|
|
1417
|
+
nameInput.type = 'text';
|
|
1418
|
+
nameInput.className = 'key-input';
|
|
1419
|
+
nameInput.value = provider.name ?? '';
|
|
1420
|
+
|
|
1421
|
+
const urlInput = document.createElement('input');
|
|
1422
|
+
urlInput.type = 'url';
|
|
1423
|
+
urlInput.className = 'key-input';
|
|
1424
|
+
urlInput.value = custom.baseUrl ?? '';
|
|
1425
|
+
|
|
1426
|
+
const keyInput = document.createElement('input');
|
|
1427
|
+
keyInput.type = 'password';
|
|
1428
|
+
keyInput.className = 'key-input';
|
|
1429
|
+
keyInput.placeholder = 'Leave empty to keep current key';
|
|
1430
|
+
keyInput.autocomplete = 'off';
|
|
1431
|
+
|
|
1432
|
+
const headersInput = document.createElement('textarea');
|
|
1433
|
+
headersInput.className = 'key-input';
|
|
1434
|
+
headersInput.rows = 2;
|
|
1435
|
+
headersInput.placeholder = 'One per line\nX-Plan: coding';
|
|
1436
|
+
headersInput.value = Object.entries(custom.headers ?? {})
|
|
1437
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
1438
|
+
.join('\n');
|
|
1439
|
+
|
|
1440
|
+
const saveBtn = document.createElement('button');
|
|
1441
|
+
saveBtn.className = 'btn btn-primary';
|
|
1442
|
+
saveBtn.textContent = 'Save Changes';
|
|
1443
|
+
|
|
1444
|
+
const cancelBtn = document.createElement('button');
|
|
1445
|
+
cancelBtn.className = 'btn btn-ghost';
|
|
1446
|
+
cancelBtn.textContent = 'Cancel';
|
|
1447
|
+
|
|
1448
|
+
const btnRow = document.createElement('div');
|
|
1449
|
+
btnRow.style.cssText = 'display:flex;gap:8px';
|
|
1450
|
+
btnRow.appendChild(saveBtn);
|
|
1451
|
+
btnRow.appendChild(cancelBtn);
|
|
1452
|
+
|
|
1453
|
+
const feedback = document.createElement('div');
|
|
1454
|
+
feedback.className = 'key-feedback';
|
|
1455
|
+
|
|
1456
|
+
form.appendChild(row('Provider name', nameInput));
|
|
1457
|
+
form.appendChild(row('Base URL', urlInput));
|
|
1458
|
+
form.appendChild(row('API key', keyInput));
|
|
1459
|
+
form.appendChild(row('Custom headers (replaces all; empty removes all)', headersInput));
|
|
1460
|
+
form.appendChild(btnRow);
|
|
1461
|
+
form.appendChild(feedback);
|
|
1462
|
+
|
|
1463
|
+
wrapper.appendChild(editBtn);
|
|
1464
|
+
wrapper.appendChild(form);
|
|
1465
|
+
|
|
1466
|
+
function parseHeaders(text) {
|
|
1467
|
+
const headers = {};
|
|
1468
|
+
for (const line of text.split('\n')) {
|
|
1469
|
+
const trimmed = line.trim();
|
|
1470
|
+
if (!trimmed) continue;
|
|
1471
|
+
const idx = trimmed.indexOf(':');
|
|
1472
|
+
if (idx < 1) continue;
|
|
1473
|
+
const name = trimmed.slice(0, idx).trim();
|
|
1474
|
+
const value = trimmed.slice(idx + 1).trim();
|
|
1475
|
+
if (name) headers[name] = value;
|
|
1476
|
+
}
|
|
1477
|
+
return headers;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
editBtn.addEventListener('click', () => {
|
|
1481
|
+
editBtn.style.display = 'none';
|
|
1482
|
+
form.style.display = 'flex';
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
cancelBtn.addEventListener('click', () => {
|
|
1486
|
+
form.style.display = 'none';
|
|
1487
|
+
editBtn.style.display = '';
|
|
1488
|
+
feedback.textContent = '';
|
|
1489
|
+
feedback.className = 'key-feedback';
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
async function submit(saveAnyway) {
|
|
1493
|
+
const payload = {
|
|
1494
|
+
providerId: provider.id,
|
|
1495
|
+
displayName: nameInput.value.trim(),
|
|
1496
|
+
baseUrl: urlInput.value.trim(),
|
|
1497
|
+
apiKey: keyInput.value.trim(),
|
|
1498
|
+
headers: parseHeaders(headersInput.value),
|
|
1499
|
+
saveAnyway,
|
|
1500
|
+
};
|
|
1501
|
+
if (!payload.displayName) {
|
|
1502
|
+
feedback.textContent = 'Enter a provider name.';
|
|
1503
|
+
feedback.className = 'key-feedback error';
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
if (!payload.baseUrl) {
|
|
1507
|
+
feedback.textContent = 'Enter a base URL.';
|
|
1508
|
+
feedback.className = 'key-feedback error';
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
saveBtn.disabled = true;
|
|
1512
|
+
feedback.textContent = 'Testing connection…';
|
|
1513
|
+
feedback.className = 'key-feedback muted';
|
|
1514
|
+
const result = await api('POST', '/api/providers/edit-custom', payload);
|
|
1515
|
+
saveBtn.disabled = false;
|
|
1516
|
+
return result;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
saveBtn.addEventListener('click', async () => {
|
|
1520
|
+
let result = await submit(false);
|
|
1521
|
+
if (!result) return;
|
|
1522
|
+
|
|
1523
|
+
if (!result.ok) {
|
|
1524
|
+
const message = [result.error, result.hint].filter(Boolean).join(' ');
|
|
1525
|
+
// Only a failed connection test is overridable. A blocked URL or a
|
|
1526
|
+
// non-custom provider can never be saved, so never offer the choice.
|
|
1527
|
+
if (!result.canSaveAnyway) {
|
|
1528
|
+
// "Nothing to change" is a no-op, not a failure — the CLI logs it as
|
|
1529
|
+
// info, so don't shout in red here either.
|
|
1530
|
+
const benign = result.error === 'Nothing to change.';
|
|
1531
|
+
feedback.textContent = message;
|
|
1532
|
+
feedback.className = benign ? 'key-feedback muted' : 'key-feedback error';
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if (!window.confirm(`${message}\n\nSave these settings anyway? The model list will not be refreshed.`)) {
|
|
1536
|
+
feedback.textContent = message;
|
|
1537
|
+
feedback.className = 'key-feedback error';
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
result = await submit(true);
|
|
1541
|
+
if (!result || !result.ok) {
|
|
1542
|
+
feedback.textContent = (result && result.error) || 'Update failed';
|
|
1543
|
+
feedback.className = 'key-feedback error';
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
feedback.textContent = result.modelsStale
|
|
1549
|
+
? `✓ Saved · model list may be out of date`
|
|
1550
|
+
: `✓ ${result.name} updated · ${result.count} models available`;
|
|
1551
|
+
feedback.className = result.modelsStale ? 'key-feedback muted' : 'key-feedback success';
|
|
1552
|
+
keyInput.value = '';
|
|
1553
|
+
showToast(`${result.name} updated`);
|
|
1554
|
+
state.modelsLoaded = false;
|
|
1555
|
+
await loadTemplates();
|
|
1556
|
+
await initModels();
|
|
1557
|
+
renderProviders();
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
return wrapper;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1374
1563
|
function buildDeleteProviderRow(provider) {
|
|
1375
1564
|
const wrapper = document.createElement('div');
|
|
1376
1565
|
wrapper.style.cssText = 'margin-top:12px;padding-top:12px;border-top:1px solid oklch(22% 0.015 265)';
|
|
@@ -82,16 +82,17 @@ import {
|
|
|
82
82
|
startServer,
|
|
83
83
|
summarizeServerProviders,
|
|
84
84
|
supportsClaudeTransparentMode,
|
|
85
|
+
updateCustomEndpointProvider,
|
|
85
86
|
validateCustomEndpointUrl,
|
|
86
87
|
writeSecureLogLine
|
|
87
|
-
} from "./chunk-
|
|
88
|
+
} from "./chunk-KDIY732Q.js";
|
|
88
89
|
import {
|
|
89
90
|
__toCommonJS,
|
|
90
91
|
init_provider_templates,
|
|
91
92
|
listAddableTemplates,
|
|
92
93
|
listVisibleOAuthTemplates,
|
|
93
94
|
provider_templates_exports
|
|
94
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-NYKVDBQC.js";
|
|
95
96
|
|
|
96
97
|
// src/ui-command.ts
|
|
97
98
|
import { createServer } from "http";
|
|
@@ -668,6 +669,8 @@ function handleUiApiRequest(req, res, opts = {}) {
|
|
|
668
669
|
handleAddProvider(req, res);
|
|
669
670
|
} else if (url === "/api/providers/add-custom" && req.method === "POST") {
|
|
670
671
|
handleAddCustomProvider(req, res);
|
|
672
|
+
} else if (url === "/api/providers/edit-custom" && req.method === "POST") {
|
|
673
|
+
handleEditCustomProvider(req, res);
|
|
671
674
|
} else if (url === "/api/providers/delete" && req.method === "POST") {
|
|
672
675
|
handleDeleteProvider(req, res);
|
|
673
676
|
} else if (url === "/api/providers/oauth/start" && req.method === "POST") {
|
|
@@ -738,6 +741,13 @@ async function handleGetModels(res, target, codexSubagents = false, uiMode) {
|
|
|
738
741
|
else if (target) catalog = providersForTarget(catalog, target);
|
|
739
742
|
const registry = loadRegistry();
|
|
740
743
|
const rawCountById = new Map(registry.providers.map((p2) => [p2.id, p2.modelsCache?.models.length ?? 0]));
|
|
744
|
+
const customById = new Map(
|
|
745
|
+
registry.providers.filter((rp) => rp.templateId === "custom-openai" || rp.templateId === "custom-anthropic").map((rp) => [rp.id, {
|
|
746
|
+
kind: rp.templateId === "custom-anthropic" ? "anthropic" : "openai",
|
|
747
|
+
baseUrl: rp.api.url ?? "",
|
|
748
|
+
headers: rp.api.headers ?? {}
|
|
749
|
+
}])
|
|
750
|
+
);
|
|
741
751
|
const providers = catalog.map((p2) => ({
|
|
742
752
|
id: p2.id,
|
|
743
753
|
name: p2.name,
|
|
@@ -753,6 +763,7 @@ async function handleGetModels(res, target, codexSubagents = false, uiMode) {
|
|
|
753
763
|
// that safe count with the larger raw cache count.
|
|
754
764
|
modelCount: p2.id === "github-copilot" ? p2.models.length : rawCountById.get(p2.id) ?? p2.models.length,
|
|
755
765
|
...p2.id === "github-copilot" ? { subscription: copilotSubscription(p2.providerData) } : {},
|
|
766
|
+
...customById.has(p2.id) ? { customEndpoint: customById.get(p2.id) } : {},
|
|
756
767
|
models: p2.models.map((m) => ({
|
|
757
768
|
id: m.id,
|
|
758
769
|
name: m.name,
|
|
@@ -868,7 +879,7 @@ function handleGetTemplates(res) {
|
|
|
868
879
|
async function handleAddCustomProvider(req, res) {
|
|
869
880
|
try {
|
|
870
881
|
const body = JSON.parse(await readBody(req));
|
|
871
|
-
const { kind, displayName, baseUrl, apiKey = "", headers } = body;
|
|
882
|
+
const { kind, displayName, baseUrl, apiKey = "", headers, confirmDuplicate } = body;
|
|
872
883
|
if (kind !== "openai" && kind !== "anthropic") {
|
|
873
884
|
sendJson(res, 400, { error: 'kind must be "openai" or "anthropic"' });
|
|
874
885
|
return;
|
|
@@ -887,12 +898,53 @@ async function handleAddCustomProvider(req, res) {
|
|
|
887
898
|
baseUrl: baseUrl.trim(),
|
|
888
899
|
apiKey: apiKey.trim(),
|
|
889
900
|
allowInsecureLocal: true,
|
|
890
|
-
headers: headers && Object.keys(headers).length > 0 ? headers : void 0
|
|
901
|
+
headers: headers && Object.keys(headers).length > 0 ? headers : void 0,
|
|
902
|
+
confirmDuplicate: confirmDuplicate === true
|
|
891
903
|
});
|
|
892
904
|
if (result.added) {
|
|
893
905
|
sendJson(res, 200, { ok: true, name: displayName.trim(), count: result.modelCount ?? 0 });
|
|
894
906
|
} else {
|
|
895
|
-
sendJson(res, 200, {
|
|
907
|
+
sendJson(res, 200, {
|
|
908
|
+
ok: false,
|
|
909
|
+
error: result.error,
|
|
910
|
+
hint: result.hint,
|
|
911
|
+
...result.duplicateOf ? { duplicateOf: result.duplicateOf } : {}
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
} catch (err) {
|
|
915
|
+
sendJson(res, 500, { error: String(err) });
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async function handleEditCustomProvider(req, res) {
|
|
919
|
+
try {
|
|
920
|
+
const body = JSON.parse(await readBody(req));
|
|
921
|
+
if (!body.providerId?.trim()) {
|
|
922
|
+
sendJson(res, 400, { error: "providerId required" });
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const result = await updateCustomEndpointProvider({
|
|
926
|
+
providerId: body.providerId.trim(),
|
|
927
|
+
displayName: body.displayName?.trim(),
|
|
928
|
+
baseUrl: body.baseUrl?.trim(),
|
|
929
|
+
apiKey: body.apiKey?.trim(),
|
|
930
|
+
headers: body.headers,
|
|
931
|
+
allowInsecureLocal: true,
|
|
932
|
+
saveAnyway: body.saveAnyway === true
|
|
933
|
+
});
|
|
934
|
+
if (result.updated) {
|
|
935
|
+
sendJson(res, 200, {
|
|
936
|
+
ok: true,
|
|
937
|
+
name: result.provider?.name ?? body.providerId,
|
|
938
|
+
count: result.modelCount ?? 0,
|
|
939
|
+
...result.modelsStale ? { modelsStale: true } : {}
|
|
940
|
+
});
|
|
941
|
+
} else {
|
|
942
|
+
sendJson(res, 200, {
|
|
943
|
+
ok: false,
|
|
944
|
+
error: result.error,
|
|
945
|
+
hint: result.hint,
|
|
946
|
+
...result.canSaveAnyway ? { canSaveAnyway: true } : {}
|
|
947
|
+
});
|
|
896
948
|
}
|
|
897
949
|
} catch (err) {
|
|
898
950
|
sendJson(res, 500, { error: String(err) });
|
|
@@ -906,7 +958,7 @@ async function handleAddProvider(req, res) {
|
|
|
906
958
|
sendJson(res, 400, { error: "templateId required" });
|
|
907
959
|
return;
|
|
908
960
|
}
|
|
909
|
-
const { listSupportedTemplates } = await import("./provider-templates-
|
|
961
|
+
const { listSupportedTemplates } = await import("./provider-templates-CGWE66TD.js");
|
|
910
962
|
const template = listSupportedTemplates().find((t) => t.id === templateId);
|
|
911
963
|
if (!template) {
|
|
912
964
|
sendJson(res, 404, { error: `Template '${templateId}' not found` });
|
|
@@ -1758,4 +1810,4 @@ export {
|
|
|
1758
1810
|
resolveUiShutdownDecision,
|
|
1759
1811
|
runUiCommand
|
|
1760
1812
|
};
|
|
1761
|
-
//# sourceMappingURL=ui-command-
|
|
1813
|
+
//# sourceMappingURL=ui-command-OIY4243G.js.map
|