@jacobbd/relay-ai 0.11.0 → 0.11.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.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ addManualModel,
4
+ removeManualModel
5
+ } from "./chunk-VXQ7ZVTW.js";
6
+ import "./chunk-O2XZFXHG.js";
7
+ import "./chunk-JIDIH7DS.js";
8
+ export {
9
+ addManualModel,
10
+ removeManualModel
11
+ };
12
+ //# sourceMappingURL=manual-models-VEBTRQFK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -7,7 +7,8 @@ import {
7
7
  listAddableTemplates,
8
8
  listSupportedTemplates,
9
9
  listVisibleOAuthTemplates
10
- } from "./chunk-SFN33VAE.js";
10
+ } from "./chunk-TRM2WGI6.js";
11
+ import "./chunk-JIDIH7DS.js";
11
12
  init_provider_templates();
12
13
  export {
13
14
  PROVIDER_TEMPLATES,
@@ -17,4 +18,4 @@ export {
17
18
  listSupportedTemplates,
18
19
  listVisibleOAuthTemplates
19
20
  };
20
- //# sourceMappingURL=provider-templates-TZOM62WC.js.map
21
+ //# sourceMappingURL=provider-templates-JY3NXZK7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -31,6 +31,7 @@ const state = {
31
31
  providerModelMinCtx: 0,
32
32
  providerModelFreeOnly: false,
33
33
  providerModelPage: 1,
34
+ manualModelForms: new Map(), // provider ID → draft and in-flight action, survives browser re-renders
34
35
  modelFilter: '',
35
36
  modelFreeOnly: false,
36
37
  agyFilter: '',
@@ -643,6 +644,7 @@ function renderProviderModelBrowser() {
643
644
  <p class="section-sub">${provider.models?.length ?? 0} models available · prices shown per 1M tokens</p>
644
645
  </div>
645
646
  </div>
647
+ ${provider.supportsManualModels ? '<div id="manual-model-panel"></div>' : ''}
646
648
  <div class="provider-browser-tools">
647
649
  <div class="search-field">
648
650
  <svg class="search-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
@@ -661,7 +663,7 @@ function renderProviderModelBrowser() {
661
663
  <span>Free models only</span>
662
664
  </label>
663
665
  </div>
664
- <button class="btn btn-ghost" id="provider-model-refresh" type="button">Refresh</button>
666
+ <button class="btn btn-ghost" id="provider-model-refresh" type="button" ${state.manualModelForms.get(provider.id)?.pending ? 'disabled' : ''}>Refresh</button>
665
667
  </div>
666
668
  <div class="provider-model-table-wrap">
667
669
  <table class="provider-model-table">
@@ -669,7 +671,7 @@ function renderProviderModelBrowser() {
669
671
  <tbody>
670
672
  ${result.items.map(model => `
671
673
  <tr>
672
- <td><strong>${escapeHtml(model.name || model.id)}</strong><code>${escapeHtml(model.id)}</code></td>
674
+ <td><strong>${escapeHtml(model.name || model.id)}</strong>${model.source === 'manual' ? ' <span class="manual-model-badge">Manual</span>' : ''}<code>${escapeHtml(model.id)}</code></td>
673
675
  <td>${fmtCtx(model.contextWindow) || '—'}</td>
674
676
  <td>${formatModelPrice(model.cost, isFreeModel(model), freeBadgeLabel(model))}</td>
675
677
  ${showActions ? `
@@ -692,6 +694,10 @@ function renderProviderModelBrowser() {
692
694
  </div>
693
695
  `;
694
696
 
697
+ if (provider.supportsManualModels) {
698
+ document.getElementById('manual-model-panel').appendChild(buildManualModelPanel(provider));
699
+ }
700
+
695
701
  document.getElementById('provider-model-search').addEventListener('input', event => {
696
702
  state.providerModelFilter = event.target.value;
697
703
  state.providerModelPage = 1;
@@ -746,6 +752,143 @@ function renderProviderModelBrowser() {
746
752
  });
747
753
  }
748
754
 
755
+ function buildManualModelPanel(provider) {
756
+ let draft = state.manualModelForms.get(provider.id);
757
+ if (!draft) {
758
+ draft = { modelId: '', displayName: '', contextWindow: '', pending: false, message: '', status: '' };
759
+ state.manualModelForms.set(provider.id, draft);
760
+ }
761
+ const panel = document.createElement('section');
762
+ panel.className = 'manual-model-panel';
763
+ const heading = document.createElement('h2');
764
+ heading.textContent = 'Add model manually';
765
+ const note = document.createElement('p');
766
+ note.className = 'section-sub';
767
+ note.id = 'manual-model-note';
768
+ note.textContent = 'Validation makes 3 small API calls and may incur provider charges. The model is saved only after all checks pass.';
769
+ const form = document.createElement('form');
770
+ form.className = 'manual-model-form';
771
+ form.setAttribute('aria-describedby', note.id);
772
+ for (const [name, labelText, type] of [
773
+ ['modelId', 'Model ID (required)', 'text'],
774
+ ['displayName', 'Display name (optional)', 'text'],
775
+ ['contextWindow', 'Context tokens (optional)', 'number'],
776
+ ]) {
777
+ const label = document.createElement('label');
778
+ label.textContent = labelText;
779
+ const input = document.createElement('input');
780
+ input.className = 'key-input';
781
+ input.name = name;
782
+ input.type = type;
783
+ input.value = draft[name];
784
+ input.disabled = draft.pending;
785
+ input.required = name === 'modelId';
786
+ if (type === 'number') { input.min = '1'; input.step = '1'; input.max = String(Number.MAX_SAFE_INTEGER); }
787
+ input.addEventListener('input', () => { draft[name] = input.value; });
788
+ label.appendChild(input);
789
+ form.appendChild(label);
790
+ }
791
+ const addButton = document.createElement('button');
792
+ addButton.type = 'submit';
793
+ addButton.className = 'btn btn-primary';
794
+ addButton.textContent = draft.pending ? 'Please wait…' : 'Test & Add';
795
+ addButton.disabled = draft.pending;
796
+ form.appendChild(addButton);
797
+ const feedback = document.createElement('div');
798
+ feedback.className = `key-feedback ${draft.status}`;
799
+ feedback.setAttribute('role', 'status');
800
+ feedback.setAttribute('aria-live', 'polite');
801
+ feedback.textContent = draft.message;
802
+ panel.append(heading, note, form, feedback);
803
+
804
+ async function perform(action, modelId, payload = {}) {
805
+ if (draft.pending) return;
806
+ draft.pending = true;
807
+ draft.status = 'muted';
808
+ draft.message = action === 'add' ? 'Validating model with the provider… This may take a moment.' : 'Removing manual entry…';
809
+ renderProviderModelBrowser();
810
+ try {
811
+ const result = await api('POST', `/api/providers/models/${action}`, { providerId: provider.id, modelId, ...payload });
812
+ if (!result.ok) {
813
+ draft.status = 'error';
814
+ draft.message = result.error || 'The operation failed. Please try again.';
815
+ return;
816
+ }
817
+ draft.status = 'success';
818
+ draft.message = action === 'add' ? `✓ ${modelId} validated and added.` : `✓ Manual entry for ${modelId} removed.`;
819
+ if (action === 'add') {
820
+ draft.modelId = '';
821
+ draft.displayName = '';
822
+ draft.contextWindow = '';
823
+ if (state.activeProviderId === provider.id) {
824
+ state.providerModelFilter = '';
825
+ state.providerModelMinCtx = 0;
826
+ state.providerModelFreeOnly = false;
827
+ state.providerModelPage = 1;
828
+ }
829
+ }
830
+ state.appModelsByTarget = {};
831
+ await initModels();
832
+ if (state.modelsError) draft.message += ' Catalog reload failed; refresh the model list to see the change.';
833
+ renderProviders();
834
+ renderFavList();
835
+ if (!isServerAdminUi()) {
836
+ renderCodexSubagentList();
837
+ renderAgyList();
838
+ renderApps();
839
+ }
840
+ } catch {
841
+ draft.status = 'error';
842
+ draft.message = 'Could not complete the request. Refresh the model list to check whether it saved before retrying.';
843
+ } finally {
844
+ draft.pending = false;
845
+ renderProviderModelBrowser();
846
+ }
847
+ }
848
+
849
+ form.addEventListener('submit', event => {
850
+ event.preventDefault();
851
+ if (draft.pending || !form.reportValidity()) return;
852
+ const modelId = draft.modelId.trim();
853
+ const contextWindow = draft.contextWindow === '' ? undefined : Number(draft.contextWindow);
854
+ if (!modelId || (contextWindow !== undefined && (!Number.isSafeInteger(contextWindow) || contextWindow <= 0))) {
855
+ draft.status = 'error';
856
+ draft.message = 'Enter a model ID and, if provided, a positive whole number of context tokens.';
857
+ renderProviderModelBrowser();
858
+ return;
859
+ }
860
+ void perform('add', modelId, {
861
+ ...(draft.displayName.trim() ? { displayName: draft.displayName.trim() } : {}),
862
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
863
+ });
864
+ });
865
+
866
+ for (const model of provider.manualModels ?? []) {
867
+ const row = document.createElement('div');
868
+ row.className = 'manual-model-entry';
869
+ const badge = document.createElement('span');
870
+ badge.className = 'manual-model-badge';
871
+ badge.textContent = 'Manual';
872
+ const label = document.createElement('span');
873
+ label.className = 'manual-model-entry-name';
874
+ label.textContent = `${model.name || model.id}${model.name && model.name !== model.id ? ` (${model.id})` : ''}`;
875
+ label.title = model.validatedAt ? `Validated ${model.validatedAt}` : '';
876
+ const removeButton = document.createElement('button');
877
+ removeButton.type = 'button';
878
+ removeButton.className = 'btn btn-ghost';
879
+ removeButton.textContent = 'Remove manual entry';
880
+ removeButton.setAttribute('aria-label', `Remove manual entry for ${model.id}`);
881
+ removeButton.disabled = draft.pending;
882
+ removeButton.addEventListener('click', () => {
883
+ if (draft.pending || !window.confirm(`Remove the manual entry for ${model.id}? If the provider also lists this model, its discovered entry will remain.`)) return;
884
+ void perform('remove', model.id);
885
+ });
886
+ row.append(badge, label, removeButton);
887
+ panel.appendChild(row);
888
+ }
889
+ return panel;
890
+ }
891
+
749
892
  function buildTemplateCard(template) {
750
893
  const [c1, c2] = providerPalette(template.id);
751
894
  const card = document.createElement('div');
@@ -1383,6 +1526,14 @@ function buildProviderBodyContent(provider) {
1383
1526
  setTimeout(() => { feedback.textContent = ''; feedback.className = 'key-feedback'; }, 4000);
1384
1527
  });
1385
1528
 
1529
+ if (provider.supportsManualModels) {
1530
+ const manualButton = document.createElement('button');
1531
+ manualButton.type = 'button';
1532
+ manualButton.className = 'btn btn-ghost';
1533
+ manualButton.textContent = 'Add model manually';
1534
+ manualButton.addEventListener('click', () => openProviderModelBrowser(provider.id));
1535
+ content.appendChild(manualButton);
1536
+ }
1386
1537
  if (provider.customEndpoint) content.appendChild(buildEditCustomEndpointRow(provider));
1387
1538
  content.appendChild(buildDeleteProviderRow(provider));
1388
1539
  return content;
@@ -1469,6 +1469,34 @@ input { font-family: inherit; }
1469
1469
  .key-feedback.error { color: var(--error); }
1470
1470
  .key-feedback.muted { color: var(--text-3); }
1471
1471
 
1472
+ .manual-model-panel {
1473
+ margin-bottom: 24px;
1474
+ padding: 18px;
1475
+ background: var(--surface-2);
1476
+ border: 1px solid var(--border);
1477
+ border-radius: 12px;
1478
+ }
1479
+ .manual-model-panel h2 { margin: 0 0 8px; font-size: 16px; }
1480
+ .manual-model-form {
1481
+ display: flex;
1482
+ align-items: end;
1483
+ gap: 12px;
1484
+ flex-wrap: wrap;
1485
+ margin-top: 16px;
1486
+ }
1487
+ .manual-model-form label { flex: 1 1 180px; font-size: 12px; color: var(--text-2); }
1488
+ .manual-model-form .key-input { display: block; width: 100%; margin-top: 6px; }
1489
+ .manual-model-badge {
1490
+ display: inline-block;
1491
+ padding: 2px 7px;
1492
+ border: 1px solid var(--border);
1493
+ border-radius: 6px;
1494
+ font-size: 11px;
1495
+ color: var(--text-2);
1496
+ }
1497
+ .manual-model-entry { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
1498
+ .manual-model-entry-name { flex: 1 1 180px; overflow-wrap: anywhere; font-size: 13px; }
1499
+
1472
1500
  .oauth-device-note {
1473
1501
  color: var(--text-2);
1474
1502
  font-size: 14px;
@@ -1,15 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- BACKENDS,
4
- CODEX_SUBAGENT_MODEL_CAP,
5
- MAX_MODEL_CATALOG,
6
- VERSION,
7
3
  addCustomEndpointProvider,
8
4
  addProviderFromTemplate,
9
- buildAntigravityAuthUrl,
10
5
  buildDedupedModelRows,
11
6
  checkForUpdates,
12
- completeAntigravityExchange,
13
7
  copilotPlanTier,
14
8
  createGatewayModelCatalog,
15
9
  ensureOpencodeCloudProviders,
@@ -24,7 +18,6 @@ import {
24
18
  formatGatewayUrls,
25
19
  freeStatusLabel,
26
20
  gatewayProviderLabel,
27
- getAppHome,
28
21
  getAppPathOverride,
29
22
  getEnvServerPassword,
30
23
  getSavedServerPassword,
@@ -35,41 +28,26 @@ import {
35
28
  getServerListenMode,
36
29
  getServerMaskGatewayIds,
37
30
  getUiDebugLogPath,
38
- guiCallbackRedirectUri,
39
31
  hostFromHeader,
40
32
  loadPreferences,
41
- loadRegistry,
42
33
  loadServerModels,
43
34
  makeTraceLogger,
44
35
  normalizeFavoriteModels,
45
- openAiDeviceCodeUrl,
46
36
  openAiIdCollisions,
47
- pollClinePassDeviceCode,
48
- pollGithubDeviceCodeToken,
49
- pollOpenAiDeviceCodeToken,
50
- pollXaiDeviceCodeToken,
51
- preferredRelayCredentialAuthRef,
52
37
  providerOptionsFromCatalog,
53
38
  providersForCodexSubagents,
54
39
  providersForTarget,
55
40
  readBody,
56
- readStoredProviderCredential,
57
41
  recordLaunchFolder,
58
42
  refreshAllProviderModels,
59
43
  refreshProviderModels,
60
44
  removeProviderFromRegistry,
61
- requestClinePassDeviceCode,
62
- requestGithubDeviceCode,
63
- requestOpenAiDeviceCode,
64
- requestXaiDeviceCode,
65
45
  resolveAdvertiseAddresses,
66
46
  resolveAdvertiseGatewayPort,
67
- resolveProviderCredential,
68
47
  resolveServerAutostart,
69
48
  resolveServerUpstreamApiKey,
70
49
  saveNativeOAuthCredential,
71
50
  savePreferences,
72
- saveProviderCredential,
73
51
  sendJson,
74
52
  setAppPathOverride,
75
53
  setSavedServerPassword,
@@ -83,16 +61,44 @@ import {
83
61
  summarizeServerProviders,
84
62
  supportsClaudeTransparentMode,
85
63
  updateCustomEndpointProvider,
86
- validateCustomEndpointUrl,
87
64
  writeSecureLogLine
88
- } from "./chunk-G6QWYYAY.js";
65
+ } from "./chunk-YD6A3ZB3.js";
89
66
  import {
90
- __toCommonJS,
91
67
  init_provider_templates,
92
68
  listAddableTemplates,
93
69
  listVisibleOAuthTemplates,
94
70
  provider_templates_exports
95
- } from "./chunk-SFN33VAE.js";
71
+ } from "./chunk-TRM2WGI6.js";
72
+ import {
73
+ BACKENDS,
74
+ CODEX_SUBAGENT_MODEL_CAP,
75
+ MAX_MODEL_CATALOG,
76
+ VERSION,
77
+ buildAntigravityAuthUrl,
78
+ completeAntigravityExchange,
79
+ getAppHome,
80
+ getProviderModels,
81
+ guiCallbackRedirectUri,
82
+ loadRegistry,
83
+ openAiDeviceCodeUrl,
84
+ pollClinePassDeviceCode,
85
+ pollGithubDeviceCodeToken,
86
+ pollOpenAiDeviceCodeToken,
87
+ pollXaiDeviceCodeToken,
88
+ preferredRelayCredentialAuthRef,
89
+ readStoredProviderCredential,
90
+ requestClinePassDeviceCode,
91
+ requestGithubDeviceCode,
92
+ requestOpenAiDeviceCode,
93
+ requestXaiDeviceCode,
94
+ resolveProviderCredential,
95
+ saveProviderCredential,
96
+ supportsManualModels,
97
+ validateCustomEndpointUrl
98
+ } from "./chunk-O2XZFXHG.js";
99
+ import {
100
+ __toCommonJS
101
+ } from "./chunk-JIDIH7DS.js";
96
102
 
97
103
  // src/ui-command.ts
98
104
  import { createServer } from "http";
@@ -673,6 +679,10 @@ function handleUiApiRequest(req, res, opts = {}) {
673
679
  handleEditCustomProvider(req, res);
674
680
  } else if (url === "/api/providers/delete" && req.method === "POST") {
675
681
  handleDeleteProvider(req, res);
682
+ } else if (url === "/api/providers/models/add" && req.method === "POST") {
683
+ handleManualModel(req, res, "add");
684
+ } else if (url === "/api/providers/models/remove" && req.method === "POST") {
685
+ handleManualModel(req, res, "remove");
676
686
  } else if (url === "/api/providers/oauth/start" && req.method === "POST") {
677
687
  handleOAuthStart(req, res);
678
688
  } else if (url.startsWith("/api/providers/oauth/status") && req.method === "GET") {
@@ -740,7 +750,19 @@ async function handleGetModels(res, target, codexSubagents = false, uiMode) {
740
750
  if (codexSubagents) catalog = providersForCodexSubagents(catalog);
741
751
  else if (target) catalog = providersForTarget(catalog, target);
742
752
  const registry = loadRegistry();
743
- const rawCountById = new Map(registry.providers.map((p2) => [p2.id, p2.modelsCache?.models.length ?? 0]));
753
+ const registryById = new Map(registry.providers.map((p2) => [p2.id, p2]));
754
+ const rawCountById = new Map(registry.providers.map((p2) => [p2.id, getProviderModels(p2).length]));
755
+ const manualIdsByProvider = new Map(registry.providers.map((p2) => [
756
+ p2.id,
757
+ new Set(getProviderModels(p2).filter((m) => m.source === "manual").map((m) => m.id))
758
+ ]));
759
+ const manualMetadata = (id) => {
760
+ const provider = registryById.get(id);
761
+ return {
762
+ supportsManualModels: provider ? provider.enabled && supportsManualModels(provider) : false,
763
+ manualModels: (provider?.manualModels ?? []).map(publicManualModel)
764
+ };
765
+ };
744
766
  const customById = new Map(
745
767
  registry.providers.filter((rp) => rp.templateId === "custom-openai" || rp.templateId === "custom-anthropic").map((rp) => [rp.id, {
746
768
  kind: rp.templateId === "custom-anthropic" ? "anthropic" : "openai",
@@ -759,6 +781,7 @@ async function handleGetModels(res, target, codexSubagents = false, uiMode) {
759
781
  return getTemplateById(t)?.anonymousFreeModels === true;
760
782
  })(),
761
783
  authType: p2.authType ?? "api",
784
+ ...manualMetadata(p2.id),
762
785
  // Copilot's runtime catalog is policy-filtered by account plan. Never replace
763
786
  // that safe count with the larger raw cache count.
764
787
  modelCount: p2.id === "github-copilot" ? p2.models.length : rawCountById.get(p2.id) ?? p2.models.length,
@@ -772,7 +795,8 @@ async function handleGetModels(res, target, codexSubagents = false, uiMode) {
772
795
  freeLabel: freeStatusLabel(m.freeStatus),
773
796
  contextWindow: m.contextWindow,
774
797
  cost: m.cost,
775
- claudeTransparentCompatible: supportsClaudeTransparentMode(m)
798
+ claudeTransparentCompatible: supportsClaudeTransparentMode(m),
799
+ ...manualIdsByProvider.get(p2.id)?.has(m.id) ? { source: "manual" } : {}
776
800
  }))
777
801
  }));
778
802
  const materializedIds = new Set(catalog.map((p2) => p2.id));
@@ -787,16 +811,80 @@ async function handleGetModels(res, target, codexSubagents = false, uiMode) {
787
811
  hasKey: true,
788
812
  freeAccess: false,
789
813
  authType: "oauth",
814
+ ...manualMetadata(rp.id),
790
815
  modelCount: 0,
791
816
  ...rp.id === "github-copilot" ? { subscription: copilotSubscription(void 0) } : {},
792
817
  models: []
793
818
  });
794
819
  }
820
+ if (!target && !codexSubagents) {
821
+ for (const rp of registry.providers) {
822
+ if (!rp.enabled || !supportsManualModels(rp) || materializedIds.has(rp.id) || getProviderModels(rp).length !== 0) continue;
823
+ const credential = await resolveProviderCredential(rp.id, rp.authRef).catch(() => null);
824
+ providers.push({
825
+ id: rp.id,
826
+ name: rp.name,
827
+ favoriteName: favoriteProviderDisplayName({ id: rp.id, name: rp.name, authType: rp.authType }),
828
+ hasKey: Boolean(credential),
829
+ freeAccess: false,
830
+ authType: rp.authType ?? "api",
831
+ modelCount: 0,
832
+ ...manualMetadata(rp.id),
833
+ ...customById.has(rp.id) ? { customEndpoint: customById.get(rp.id) } : {},
834
+ models: []
835
+ });
836
+ }
837
+ }
795
838
  sendJson(res, 200, { providers });
796
839
  } catch (err) {
797
840
  sendCatalogFetchError(res, err, "Model fetch");
798
841
  }
799
842
  }
843
+ function publicManualModel(model) {
844
+ return {
845
+ id: model.id,
846
+ name: model.name,
847
+ contextWindow: model.contextWindow,
848
+ validatedAt: model.validatedAt,
849
+ source: "manual"
850
+ };
851
+ }
852
+ async function handleManualModel(req, res, action) {
853
+ let body;
854
+ try {
855
+ const parsed = JSON.parse(await readBody(req));
856
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("object required");
857
+ body = parsed;
858
+ } catch {
859
+ sendJson(res, 400, { ok: false, error: "Request body must be a JSON object" });
860
+ return;
861
+ }
862
+ const { providerId, modelId, displayName, contextWindow } = body;
863
+ if (typeof providerId !== "string" || !providerId.trim() || typeof modelId !== "string" || !modelId.trim()) {
864
+ sendJson(res, 400, { ok: false, error: "providerId and modelId must be non-empty strings" });
865
+ return;
866
+ }
867
+ if (action === "add" && (displayName !== void 0 && typeof displayName !== "string" || contextWindow !== void 0 && (typeof contextWindow !== "number" || !Number.isSafeInteger(contextWindow) || contextWindow <= 0))) {
868
+ sendJson(res, 400, { ok: false, error: "displayName must be a string and contextWindow must be a positive integer when provided" });
869
+ return;
870
+ }
871
+ try {
872
+ const { addManualModel, removeManualModel } = await import("./manual-models-VEBTRQFK.js");
873
+ const result = action === "add" ? await addManualModel({
874
+ providerId: providerId.trim(),
875
+ modelId: modelId.trim(),
876
+ ...typeof displayName === "string" && displayName.trim() ? { displayName: displayName.trim() } : {},
877
+ ...typeof contextWindow === "number" ? { contextWindow } : {}
878
+ }) : removeManualModel(providerId.trim(), modelId.trim());
879
+ sendJson(res, 200, {
880
+ ok: result.ok,
881
+ ...!result.ok ? { error: result.error ?? "Manual model operation failed" } : {},
882
+ ...result.ok && "model" in result && result.model ? { model: publicManualModel(result.model) } : {}
883
+ });
884
+ } catch {
885
+ sendJson(res, 500, { ok: false, error: `Unable to ${action} manual model. Please try again.` });
886
+ }
887
+ }
800
888
  function copilotSubscription(providerData) {
801
889
  const tier = copilotPlanTier(providerData);
802
890
  if (tier === "free") return { tier, label: "Copilot Free" };
@@ -958,7 +1046,7 @@ async function handleAddProvider(req, res) {
958
1046
  sendJson(res, 400, { error: "templateId required" });
959
1047
  return;
960
1048
  }
961
- const { listSupportedTemplates } = await import("./provider-templates-TZOM62WC.js");
1049
+ const { listSupportedTemplates } = await import("./provider-templates-JY3NXZK7.js");
962
1050
  const template = listSupportedTemplates().find((t) => t.id === templateId);
963
1051
  if (!template) {
964
1052
  sendJson(res, 404, { error: `Template '${templateId}' not found` });
@@ -1810,4 +1898,4 @@ export {
1810
1898
  resolveUiShutdownDecision,
1811
1899
  runUiCommand
1812
1900
  };
1813
- //# sourceMappingURL=ui-command-RJVKDGGG.js.map
1901
+ //# sourceMappingURL=ui-command-WKPJI2CU.js.map