@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.
@@ -7,7 +7,7 @@ import {
7
7
  listAddableTemplates,
8
8
  listSupportedTemplates,
9
9
  listVisibleOAuthTemplates
10
- } from "./chunk-Q2FTCICO.js";
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-XKNRKAQU.js.map
20
+ //# sourceMappingURL=provider-templates-CGWE66TD.js.map
@@ -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
- const result = await api('POST', '/api/providers/add-custom', { kind, displayName, baseUrl, apiKey, headers });
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-GQCFLSEM.js";
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-Q2FTCICO.js";
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, { ok: false, error: result.error, hint: result.hint });
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-XKNRKAQU.js");
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-SDMYDYT6.js.map
1813
+ //# sourceMappingURL=ui-command-OIY4243G.js.map