@jacobbd/relay-ai 0.6.0 → 0.6.2

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-MVBA7ABV.js";
10
+ } from "./chunk-EJONCU3B.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-6XYKAZB5.js.map
20
+ //# sourceMappingURL=provider-templates-BPGB5V2L.js.map
@@ -41,6 +41,7 @@ const state = {
41
41
  status: null,
42
42
  error: null,
43
43
  starting: false,
44
+ modelSearch: '',
44
45
  form: {
45
46
  seeded: false,
46
47
  expose: 'favorites', // 'favorites' | 'specific'
@@ -53,6 +54,7 @@ const state = {
53
54
  listenMode: 'local', // 'local' | 'network'
54
55
  passwordMode: 'new', // 'saved' | 'new'
55
56
  password: '',
57
+ passwordVisible: false,
56
58
  savePassword: false,
57
59
  },
58
60
  },
@@ -1059,6 +1061,8 @@ function buildProviderBodyContent(provider) {
1059
1061
  const countEl = document.querySelector(`[data-id="${CSS.escape(provider.id)}"] .provider-models-count`);
1060
1062
  if (countEl) countEl.textContent = `${result.count} models`;
1061
1063
  provider.modelCount = result.count;
1064
+ state.modelsLoaded = false;
1065
+ await initModels();
1062
1066
  } else {
1063
1067
  feedback.textContent = result.error ?? 'Refresh failed';
1064
1068
  feedback.className = 'key-feedback error';
@@ -1179,6 +1183,8 @@ function buildOAuthProviderBodyContent(provider) {
1179
1183
  feedback.className = 'key-feedback success';
1180
1184
  const countEl = document.querySelector(`[data-id="${CSS.escape(provider.id)}"] .provider-models-count`);
1181
1185
  if (countEl) countEl.textContent = `${result.count} models`;
1186
+ state.modelsLoaded = false;
1187
+ await initModels();
1182
1188
  } else {
1183
1189
  feedback.textContent = result.error ?? 'Refresh failed';
1184
1190
  feedback.className = 'key-feedback error';
@@ -1612,6 +1618,18 @@ async function initModels() {
1612
1618
  state.modelsError = String(err);
1613
1619
  state.modelsLoaded = true;
1614
1620
  }
1621
+ // Keep the Providers header counts in sync after add / delete / OAuth / refresh.
1622
+ renderProviderStats();
1623
+ // Server tab caches its provider checklist — invalidate so new providers appear without a full reload.
1624
+ invalidateServerProviderCache();
1625
+ }
1626
+
1627
+ function invalidateServerProviderCache() {
1628
+ state.server.form.providersLoaded = false;
1629
+ if (state.server.status?.running) return;
1630
+ if (state.server.form.expose !== 'specific') return;
1631
+ if (!document.getElementById('server-panel')) return;
1632
+ void loadServerProviders().then(() => renderServerPanel());
1615
1633
  }
1616
1634
 
1617
1635
  async function init() {
@@ -2264,7 +2282,14 @@ function seedServerFormFromSaved(saved) {
2264
2282
  f.maskGatewayIds = saved.maskGatewayIds;
2265
2283
  // Published Docker ports only reach 0.0.0.0 inside the container.
2266
2284
  f.listenMode = isServerAdminUi() || saved.listenMode === 'network' ? 'network' : 'local';
2267
- f.passwordMode = saved.hasSavedPassword ? 'saved' : 'new';
2285
+ // Env password (Docker): prefill the field so Start works and clients can copy it.
2286
+ // Keychain-saved password: keep "Use saved" (value never sent to the browser).
2287
+ if (saved.prefillPassword && !f.password) {
2288
+ f.password = saved.prefillPassword;
2289
+ f.passwordMode = 'new';
2290
+ } else {
2291
+ f.passwordMode = saved.hasSavedPassword ? 'saved' : 'new';
2292
+ }
2268
2293
  if (f.expose === 'specific') loadServerProviders().then(renderServerPanel);
2269
2294
  }
2270
2295
 
@@ -2305,6 +2330,10 @@ function renderServerPanel() {
2305
2330
  panel.innerHTML = '<div class="skeleton skeleton-card"></div>';
2306
2331
  return;
2307
2332
  }
2333
+ // Lazy-load (or re-load after invalidate) when the specific-provider checklist is visible.
2334
+ if (!s.status.running && s.form.expose === 'specific' && !s.form.providersLoaded) {
2335
+ void loadServerProviders().then(() => renderServerPanel());
2336
+ }
2308
2337
  panel.innerHTML = s.status.running ? renderServerRunning(s.status) : renderServerSetup(s);
2309
2338
  }
2310
2339
 
@@ -2364,16 +2393,22 @@ function renderServerNetworkOptions() {
2364
2393
  const s = state.server;
2365
2394
  const f = s.form;
2366
2395
  const hasSaved = Boolean(s.status?.saved?.hasSavedPassword);
2396
+ const hasEnv = Boolean(s.status?.saved?.hasEnvPassword);
2367
2397
  const showPasswordInput = f.passwordMode === 'new' || !hasSaved;
2398
+ const inputType = f.passwordVisible ? 'text' : 'password';
2368
2399
 
2369
2400
  return `
2370
2401
  <div class="server-network-options">
2371
- ${hasSaved ? toggleGroupHtml([
2402
+ ${hasSaved && !hasEnv ? toggleGroupHtml([
2372
2403
  { label: 'Use saved password', active: f.passwordMode === 'saved', onClick: "setServerPasswordMode('saved')" },
2373
2404
  { label: 'Enter new password', active: f.passwordMode === 'new', onClick: "setServerPasswordMode('new')" },
2374
2405
  ]) : ''}
2406
+ ${hasEnv && f.passwordMode === 'new' ? '<div class="server-field-hint">Prefilled from RELAY_AI_SERVER_PASSWORD</div>' : ''}
2375
2407
  ${showPasswordInput ? `
2376
- <input type="password" class="key-input" placeholder="Server password" value="${escapeHtml(f.password)}" oninput="setServerPassword(this.value)">
2408
+ <div class="server-password-row">
2409
+ <input type="${inputType}" class="key-input" id="server-password-input" placeholder="Server password" value="${escapeHtml(f.password)}" oninput="setServerPassword(this.value)" autocomplete="off">
2410
+ <button type="button" class="btn btn-ghost server-password-reveal" onclick="toggleServerPasswordVisible()" aria-pressed="${f.passwordVisible ? 'true' : 'false'}">${f.passwordVisible ? 'Hide' : 'Reveal'}</button>
2411
+ </div>
2377
2412
  <label class="server-toggle-row">
2378
2413
  <input type="checkbox" ${f.savePassword ? 'checked' : ''} onchange="setServerSavePassword(this.checked)">
2379
2414
  <span>Save this password for next time</span>
@@ -2502,15 +2537,6 @@ function renderServerRunning(status) {
2502
2537
  configBits.push(status.maskGatewayIds ? 'Discovery ids masked' : 'Discovery ids raw');
2503
2538
  configBits.push(status.listenMode === 'network' ? 'Network' : 'Local only');
2504
2539
 
2505
- const modelRows = (status.models ?? []).map(m => `
2506
- <tr>
2507
- <td>${escapeHtml(m.providerLabel)}</td>
2508
- <td>${escapeHtml(m.name)}</td>
2509
- <td>${serverIdCell(m.anthropicId)}</td>
2510
- <td>${serverIdCell(m.openaiId)}</td>
2511
- </tr>
2512
- `).join('');
2513
-
2514
2540
  return `
2515
2541
  <div class="server-running">
2516
2542
  <div class="server-status-row">
@@ -2524,16 +2550,63 @@ function renderServerRunning(status) {
2524
2550
  ${urlCards.join('')}
2525
2551
  ${apiKeyCard}
2526
2552
  </div>
2553
+ <div class="server-models-header">
2554
+ <div class="search-field">
2555
+ <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>
2556
+ <input type="search" class="search-input" placeholder="Search exposed models…" value="${escapeHtml(state.server.modelSearch)}" oninput="setServerModelSearch(this.value)">
2557
+ </div>
2558
+ <span class="server-models-count" id="server-models-count">${serverModelsCountText()}</span>
2559
+ </div>
2527
2560
  <div class="server-model-table-wrap">
2528
2561
  <table class="server-model-table">
2529
2562
  <thead><tr><th>Provider</th><th>Model</th><th>Anthropic ID</th><th>OpenAI ID</th></tr></thead>
2530
- <tbody>${modelRows || '<tr><td colspan="4" class="fav-empty">No models exposed.</td></tr>'}</tbody>
2563
+ <tbody id="server-model-tbody">${buildServerModelRows()}</tbody>
2531
2564
  </table>
2532
2565
  </div>
2533
2566
  </div>
2534
2567
  `;
2535
2568
  }
2536
2569
 
2570
+ function filteredServerModels() {
2571
+ const models = state.server.status?.models ?? [];
2572
+ const q = state.server.modelSearch.trim().toLowerCase();
2573
+ if (!q) return models;
2574
+ return models.filter(m =>
2575
+ m.providerLabel.toLowerCase().includes(q)
2576
+ || m.name.toLowerCase().includes(q)
2577
+ || m.anthropicId.toLowerCase().includes(q)
2578
+ || m.openaiId.toLowerCase().includes(q)
2579
+ );
2580
+ }
2581
+
2582
+ function buildServerModelRows() {
2583
+ const rows = filteredServerModels().map(m => `
2584
+ <tr>
2585
+ <td>${escapeHtml(m.providerLabel)}</td>
2586
+ <td>${escapeHtml(m.name)}</td>
2587
+ <td>${serverIdCell(m.anthropicId)}</td>
2588
+ <td>${serverIdCell(m.openaiId)}</td>
2589
+ </tr>
2590
+ `).join('');
2591
+ const total = state.server.status?.models?.length ?? 0;
2592
+ if (rows) return rows;
2593
+ return `<tr><td colspan="4" class="fav-empty">${total === 0 ? 'No models exposed.' : 'No models match your search.'}</td></tr>`;
2594
+ }
2595
+
2596
+ function serverModelsCountText() {
2597
+ const total = state.server.status?.models?.length ?? 0;
2598
+ const shown = filteredServerModels().length;
2599
+ return shown === total ? `${total} model${total !== 1 ? 's' : ''}` : `Showing ${shown} of ${total} models`;
2600
+ }
2601
+
2602
+ function setServerModelSearch(value) {
2603
+ state.server.modelSearch = value;
2604
+ const tbody = document.getElementById('server-model-tbody');
2605
+ if (tbody) tbody.innerHTML = buildServerModelRows();
2606
+ const count = document.getElementById('server-models-count');
2607
+ if (count) count.textContent = serverModelsCountText();
2608
+ }
2609
+
2537
2610
  function setServerExpose(mode) {
2538
2611
  state.server.form.expose = mode;
2539
2612
  state.server.error = null;
@@ -2581,6 +2654,12 @@ function setServerPassword(value) {
2581
2654
  state.server.form.password = value;
2582
2655
  }
2583
2656
 
2657
+ function toggleServerPasswordVisible() {
2658
+ state.server.form.passwordVisible = !state.server.form.passwordVisible;
2659
+ renderServerPanel();
2660
+ document.getElementById('server-password-input')?.focus();
2661
+ }
2662
+
2584
2663
  function setServerSavePassword(checked) {
2585
2664
  state.server.form.savePassword = checked;
2586
2665
  }
@@ -2671,11 +2750,13 @@ function toggleServerApiKeyVisibility() {
2671
2750
  window.setServerExpose = setServerExpose;
2672
2751
  window.toggleServerProvider = toggleServerProvider;
2673
2752
  window.setServerProviderSearch = setServerProviderSearch;
2753
+ window.setServerModelSearch = setServerModelSearch;
2674
2754
  window.setServerMask = setServerMask;
2675
2755
  window.setServerFreeModelsOnly = setServerFreeModelsOnly;
2676
2756
  window.setServerListenMode = setServerListenMode;
2677
2757
  window.setServerPasswordMode = setServerPasswordMode;
2678
2758
  window.setServerPassword = setServerPassword;
2759
+ window.toggleServerPasswordVisible = toggleServerPasswordVisible;
2679
2760
  window.setServerSavePassword = setServerSavePassword;
2680
2761
  window.submitServerStart = submitServerStart;
2681
2762
  window.stopServerGateway = stopServerGateway;
@@ -1876,6 +1876,22 @@ input { font-family: inherit; }
1876
1876
  gap: 10px;
1877
1877
  }
1878
1878
 
1879
+ .server-password-row {
1880
+ display: flex;
1881
+ align-items: center;
1882
+ gap: 8px;
1883
+ }
1884
+
1885
+ .server-password-row .key-input {
1886
+ flex: 1;
1887
+ min-width: 0;
1888
+ }
1889
+
1890
+ .server-password-reveal {
1891
+ flex-shrink: 0;
1892
+ white-space: nowrap;
1893
+ }
1894
+
1879
1895
  .server-provider-chips {
1880
1896
  display: flex;
1881
1897
  flex-wrap: wrap;
@@ -2010,6 +2026,21 @@ input { font-family: inherit; }
2010
2026
 
2011
2027
  .url-card-value.masked { letter-spacing: 0.15em; }
2012
2028
 
2029
+ .server-models-header {
2030
+ display: flex;
2031
+ align-items: center;
2032
+ gap: 12px;
2033
+ margin-top: 8px;
2034
+ }
2035
+
2036
+ .server-models-header .search-field { flex: 1; }
2037
+
2038
+ .server-models-count {
2039
+ font-size: 14px;
2040
+ color: var(--text-3);
2041
+ white-space: nowrap;
2042
+ }
2043
+
2013
2044
  .server-model-table-wrap {
2014
2045
  border: 1px solid var(--border);
2015
2046
  border-radius: var(--radius-sm);
@@ -25,7 +25,9 @@ import {
25
25
  gatewayProviderLabel,
26
26
  getAppHome,
27
27
  getAppPathOverride,
28
+ getEnvServerPassword,
28
29
  getSavedServerPassword,
30
+ getServerDebugLogPath,
29
31
  getServerExposedProviders,
30
32
  getServerFavoritesOnly,
31
33
  getServerFreeModelsOnly,
@@ -39,6 +41,7 @@ import {
39
41
  loadServerModels,
40
42
  makeTraceLogger,
41
43
  openAiDeviceCodeUrl,
44
+ openAiIdCollisions,
42
45
  pollGithubDeviceCodeToken,
43
46
  pollOpenAiDeviceCodeToken,
44
47
  pollXaiDeviceCodeToken,
@@ -71,14 +74,14 @@ import {
71
74
  supportsClaudeTransparentMode,
72
75
  validateCustomEndpointUrl,
73
76
  writeSecureLogLine
74
- } from "./chunk-SV2Y6OCD.js";
77
+ } from "./chunk-I3I5LKZI.js";
75
78
  import {
76
79
  __toCommonJS,
77
80
  init_provider_templates,
78
81
  listAddableTemplates,
79
82
  listVisibleOAuthTemplates,
80
83
  provider_templates_exports
81
- } from "./chunk-MVBA7ABV.js";
84
+ } from "./chunk-EJONCU3B.js";
82
85
 
83
86
  // src/ui-command.ts
84
87
  import { createServer } from "http";
@@ -378,20 +381,24 @@ function buildModelRows(models, gateway) {
378
381
  if (list) list.push(model);
379
382
  else groups.set(label, [model]);
380
383
  }
384
+ const collisions = openAiIdCollisions(models);
381
385
  const rows = [];
382
386
  for (const [providerLabel, groupModels] of groups) {
383
- for (const row of buildDedupedModelRows(groupModels, gateway)) rows.push({ providerLabel, ...row });
387
+ for (const row of buildDedupedModelRows(groupModels, gateway, collisions)) rows.push({ providerLabel, ...row });
384
388
  }
385
389
  return rows.sort((a, b) => a.providerLabel.localeCompare(b.providerLabel) || a.name.localeCompare(b.name));
386
390
  }
387
391
  async function buildSavedConfig() {
392
+ const envPassword = getEnvServerPassword();
388
393
  return {
389
394
  favoritesOnly: getServerFavoritesOnly(),
390
395
  freeModelsOnly: getServerFreeModelsOnly(),
391
396
  exposedProviders: getServerExposedProviders(),
392
397
  maskGatewayIds: getServerMaskGatewayIds(),
393
398
  listenMode: getServerListenMode(),
394
- hasSavedPassword: await hasSavedPasswordCached()
399
+ hasSavedPassword: await hasSavedPasswordCached(),
400
+ hasEnvPassword: Boolean(envPassword),
401
+ ...envPassword ? { prefillPassword: envPassword } : {}
395
402
  };
396
403
  }
397
404
  async function getServerStatus(opts) {
@@ -445,16 +452,26 @@ async function doStartGatewayServer(req, opts) {
445
452
  let serverPassword = null;
446
453
  if (req.listenMode === "network") {
447
454
  if (req.passwordMode === "saved") {
448
- const saved = await getSavedServerPassword();
449
- if (!saved) return { ok: false, error: "No saved password found \u2014 enter a new password." };
450
- serverPassword = saved;
455
+ const configured = await getSavedServerPassword() ?? getEnvServerPassword();
456
+ if (!configured) {
457
+ return {
458
+ ok: false,
459
+ error: "No configured password found \u2014 set RELAY_AI_SERVER_PASSWORD, or enter a new password."
460
+ };
461
+ }
462
+ serverPassword = configured;
451
463
  } else {
452
464
  const trimmed = (req.password ?? "").trim();
453
- if (!trimmed) return { ok: false, error: "A server password is required for network mode." };
454
- serverPassword = trimmed;
455
- if (req.savePassword) {
456
- await setSavedServerPassword(trimmed);
457
- hasSavedPasswordCache = { value: true, expiresAt: Date.now() + SAVED_PASSWORD_CACHE_TTL_MS };
465
+ if (!trimmed) {
466
+ const configured = getEnvServerPassword() ?? await getSavedServerPassword();
467
+ if (!configured) return { ok: false, error: "A server password is required for network mode." };
468
+ serverPassword = configured;
469
+ } else {
470
+ serverPassword = trimmed;
471
+ if (req.savePassword) {
472
+ await setSavedServerPassword(trimmed);
473
+ hasSavedPasswordCache = { value: true, expiresAt: Date.now() + SAVED_PASSWORD_CACHE_TTL_MS };
474
+ }
458
475
  }
459
476
  }
460
477
  }
@@ -502,7 +519,8 @@ async function doStartGatewayServer(req, opts) {
502
519
  serverPassword,
503
520
  catalog: createGatewayModelCatalog(models, gateway),
504
521
  backends: BACKENDS,
505
- gateway
522
+ gateway,
523
+ debugLogPath: getServerDebugLogPath()
506
524
  });
507
525
  } catch (err) {
508
526
  const code = err?.code;
@@ -807,7 +825,7 @@ async function handleAddProvider(req, res) {
807
825
  sendJson(res, 400, { error: "templateId required" });
808
826
  return;
809
827
  }
810
- const { listSupportedTemplates } = await import("./provider-templates-6XYKAZB5.js");
828
+ const { listSupportedTemplates } = await import("./provider-templates-BPGB5V2L.js");
811
829
  const template = listSupportedTemplates().find((t) => t.id === templateId);
812
830
  if (!template) {
813
831
  sendJson(res, 404, { error: `Template '${templateId}' not found` });
@@ -1564,4 +1582,4 @@ export {
1564
1582
  resolveUiShutdownDecision,
1565
1583
  runUiCommand
1566
1584
  };
1567
- //# sourceMappingURL=ui-command-DJZIIIWC.js.map
1585
+ //# sourceMappingURL=ui-command-GNCQS7F7.js.map