@jacobbd/relay-ai 0.9.2 → 0.9.3

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/cli.js CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  confirmLaunchMessage,
49
49
  createGatewayModelCatalog,
50
50
  createLanguageModel,
51
+ customEndpointKind,
51
52
  deepMergeProviderOptions,
52
53
  detectConflicts,
53
54
  effectiveProviderBaseUrl,
@@ -193,11 +194,12 @@ import {
193
194
  thinkingProviderOptions,
194
195
  toggleProviderEnabled,
195
196
  translateRequest,
197
+ updateCustomEndpointProvider,
196
198
  upstreamHttpStatus,
197
199
  validateCustomEndpointUrl,
198
200
  writeSecureLogLine,
199
201
  zenRegistryStub
200
- } from "./chunk-GQCFLSEM.js";
202
+ } from "./chunk-PVGAE7HA.js";
201
203
  import {
202
204
  filterTemplates,
203
205
  getTemplateById,
@@ -205,7 +207,7 @@ import {
205
207
  listAddableTemplates,
206
208
  listSupportedTemplates,
207
209
  listVisibleOAuthTemplates
208
- } from "./chunk-Q2FTCICO.js";
210
+ } from "./chunk-NYKVDBQC.js";
209
211
 
210
212
  // src/cli.ts
211
213
  import pc12 from "picocolors";
@@ -1748,17 +1750,31 @@ async function runCustomEndpointAddFlow() {
1748
1750
  if (name) headers[name] = value;
1749
1751
  }
1750
1752
  }
1751
- const spinner9 = p5.spinner();
1752
- spinner9.start("Testing connection...");
1753
- const result = await addCustomEndpointProvider({
1753
+ const addInput = {
1754
1754
  displayName: String(displayName).trim(),
1755
1755
  baseUrl: String(baseUrl).trim(),
1756
1756
  apiKey: String(apiKey ?? "").trim(),
1757
1757
  kind: kindChoice,
1758
1758
  allowInsecureLocal: allowInsecureHttp,
1759
1759
  headers: Object.keys(headers).length > 0 ? headers : void 0
1760
- });
1760
+ };
1761
+ const spinner9 = p5.spinner();
1762
+ spinner9.start("Testing connection...");
1763
+ let result = await addCustomEndpointProvider(addInput);
1761
1764
  spinner9.stop("");
1765
+ if (!result.added && result.duplicateOf) {
1766
+ const addAnyway = await p5.confirm({
1767
+ message: `You already have a backend with the same URL, key and headers (${result.duplicateOf}). Add another?`,
1768
+ initialValue: false
1769
+ });
1770
+ if (p5.isCancel(addAnyway) || !addAnyway) {
1771
+ p5.cancel("Cancelled.");
1772
+ return 0;
1773
+ }
1774
+ spinner9.start("Testing connection...");
1775
+ result = await addCustomEndpointProvider({ ...addInput, confirmDuplicate: true });
1776
+ spinner9.stop("");
1777
+ }
1762
1778
  if (!result.added) {
1763
1779
  showProviderAddFailure(result.error, result.hint, "Could not add custom provider.");
1764
1780
  return 1;
@@ -1766,6 +1782,118 @@ async function runCustomEndpointAddFlow() {
1766
1782
  logConnected(result.provider?.name ?? "Provider", result.modelCount ?? 0);
1767
1783
  return 0;
1768
1784
  }
1785
+ async function runCustomEndpointEditFlow(provider) {
1786
+ const currentHeaders = provider.api.headers ?? {};
1787
+ const headerSummary = Object.keys(currentHeaders).length > 0 ? Object.entries(currentHeaders).map(([k, v]) => `${k}: ${v}`).join(", ") : "none";
1788
+ p5.log.info(`Base URL: ${provider.api.url ?? "(none)"}
1789
+ Headers: ${headerSummary}`);
1790
+ const displayName = await p5.text({
1791
+ message: "Display name:",
1792
+ initialValue: provider.name,
1793
+ validate: (v) => v.trim() ? void 0 : "Name is required"
1794
+ });
1795
+ if (p5.isCancel(displayName)) {
1796
+ p5.cancel("Cancelled.");
1797
+ return 0;
1798
+ }
1799
+ const baseUrl = await p5.text({
1800
+ message: "Base URL:",
1801
+ initialValue: provider.api.url ?? "",
1802
+ validate: (v) => v.trim() ? void 0 : "URL is required"
1803
+ });
1804
+ if (p5.isCancel(baseUrl)) {
1805
+ p5.cancel("Cancelled.");
1806
+ return 0;
1807
+ }
1808
+ const usesHttp = /^http:\/\//i.test(String(baseUrl).trim());
1809
+ let allowInsecureHttp = false;
1810
+ if (usesHttp) {
1811
+ p5.log.warn("HTTP is not encrypted. Only use it for a trusted local or LAN server, like Ollama on your own network.");
1812
+ const allowLocal = await p5.confirm({
1813
+ message: "Allow insecure HTTP for this local/LAN server?",
1814
+ initialValue: true
1815
+ });
1816
+ if (p5.isCancel(allowLocal)) return 0;
1817
+ allowInsecureHttp = allowLocal === true;
1818
+ }
1819
+ const apiKey = await p5.password({
1820
+ message: "API key (leave empty to keep the current key):"
1821
+ });
1822
+ if (p5.isCancel(apiKey)) {
1823
+ p5.cancel("Cancelled.");
1824
+ return 0;
1825
+ }
1826
+ const editHeaders = await p5.confirm({
1827
+ message: `Replace custom headers? (current: ${headerSummary})`,
1828
+ initialValue: false
1829
+ });
1830
+ if (p5.isCancel(editHeaders)) {
1831
+ p5.cancel("Cancelled.");
1832
+ return 0;
1833
+ }
1834
+ let headers;
1835
+ if (editHeaders) {
1836
+ headers = {};
1837
+ p5.log.info("Enter the full header set. Leave the first one empty to remove all headers.");
1838
+ for (; ; ) {
1839
+ const headerLine = await p5.text({
1840
+ message: "Header (leave empty when done):",
1841
+ placeholder: "X-Plan: coding"
1842
+ });
1843
+ if (p5.isCancel(headerLine)) {
1844
+ p5.cancel("Cancelled.");
1845
+ return 0;
1846
+ }
1847
+ const trimmed = String(headerLine).trim();
1848
+ if (!trimmed) break;
1849
+ const idx = trimmed.indexOf(":");
1850
+ if (idx < 1) {
1851
+ p5.log.warn('Use the format "Name: Value" \u2014 skipped.');
1852
+ continue;
1853
+ }
1854
+ const name = trimmed.slice(0, idx).trim();
1855
+ const value = trimmed.slice(idx + 1).trim();
1856
+ if (name) headers[name] = value;
1857
+ }
1858
+ }
1859
+ const runUpdate = (saveAnyway) => updateCustomEndpointProvider({
1860
+ providerId: provider.id,
1861
+ displayName: String(displayName).trim(),
1862
+ baseUrl: String(baseUrl).trim(),
1863
+ apiKey: String(apiKey ?? "").trim(),
1864
+ headers,
1865
+ allowInsecureLocal: allowInsecureHttp,
1866
+ saveAnyway
1867
+ });
1868
+ const spinner9 = p5.spinner();
1869
+ spinner9.start("Testing connection...");
1870
+ let result = await runUpdate(false);
1871
+ spinner9.stop("");
1872
+ if (!result.updated && result.error === "Nothing to change.") {
1873
+ p5.log.info("No changes made.");
1874
+ return 0;
1875
+ }
1876
+ if (!result.updated) {
1877
+ showProviderAddFailure(result.error, result.hint, "Could not update backend.");
1878
+ if (!result.canSaveAnyway) return 1;
1879
+ const saveAnyway = await p5.confirm({
1880
+ message: "Save these settings anyway? The model list will not be refreshed.",
1881
+ initialValue: false
1882
+ });
1883
+ if (p5.isCancel(saveAnyway) || !saveAnyway) return 1;
1884
+ result = await runUpdate(true);
1885
+ if (!result.updated) {
1886
+ showProviderAddFailure(result.error, result.hint, "Could not update backend.");
1887
+ return 1;
1888
+ }
1889
+ }
1890
+ if (result.modelsStale) {
1891
+ p5.log.warn(`${result.provider?.name ?? provider.name} saved, but the model list may be out of date.`);
1892
+ } else {
1893
+ logConnected(result.provider?.name ?? provider.name, result.modelCount ?? 0);
1894
+ }
1895
+ return 0;
1896
+ }
1769
1897
  async function runProvidersAdd() {
1770
1898
  const registry = loadRegistry();
1771
1899
  const hasOpencode = findOpencodeBinary() !== null;
@@ -1891,7 +2019,14 @@ async function runProviderDetail(id) {
1891
2019
  hint: "Refresh OAuth tokens or switch accounts"
1892
2020
  });
1893
2021
  }
1894
- if (provider.authType !== "oauth" && provider.authRef.startsWith("keyring:")) {
2022
+ const editableCustomKind = customEndpointKind(provider);
2023
+ if (editableCustomKind) {
2024
+ detailOptions.push({
2025
+ value: "edit-custom",
2026
+ label: "Edit backend settings",
2027
+ hint: "Name, base URL, API key, headers"
2028
+ });
2029
+ } else if (provider.authType !== "oauth" && provider.authRef.startsWith("keyring:")) {
1895
2030
  detailOptions.push({
1896
2031
  value: "change-key",
1897
2032
  label: "Change API key",
@@ -1927,6 +2062,9 @@ async function runProviderDetail(id) {
1927
2062
  if (action === "refresh") {
1928
2063
  return await runProvidersRefreshModels(id) === 0 ? "back" : "failed";
1929
2064
  }
2065
+ if (action === "edit-custom") {
2066
+ return await runCustomEndpointEditFlow(provider) === 0 ? "back" : "failed";
2067
+ }
1930
2068
  if (action === "change-key") {
1931
2069
  return await runProviderApiKeyChange(provider, registry) === 0 ? "back" : "failed";
1932
2070
  }
@@ -10488,7 +10626,8 @@ function mergeAppConfig(existing, spec) {
10488
10626
  apiBaseUrl: spec.route.baseURL,
10489
10627
  supportedParameters: spec.route.supportedParameters,
10490
10628
  reasoning: spec.route.reasoning,
10491
- interleavedReasoningField: spec.route.interleavedReasoningField
10629
+ interleavedReasoningField: spec.route.interleavedReasoningField,
10630
+ upstreamModelId: spec.route.upstreamModelId
10492
10631
  });
10493
10632
  if (caps.levels.length === 0 || !caps.levels.includes(existingEffort)) {
10494
10633
  if (caps.levels.length > 0 && caps.defaultLevel) {
@@ -14869,7 +15008,7 @@ Options:
14869
15008
  --trace Write debug logs under ~/.relay-ai/logs/`);
14870
15009
  return 0;
14871
15010
  }
14872
- const { runUiCommand } = await import("./ui-command-SDMYDYT6.js");
15011
+ const { runUiCommand } = await import("./ui-command-JQPEZAMN.js");
14873
15012
  return runUiCommand({ trace: parsed.trace, serverMode: parsed.uiServerMode });
14874
15013
  }
14875
15014
  if (parsed.command === "models") {