@jacobbd/relay-ai 0.7.2 → 0.7.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/{chunk-LNQ46VW7.js → chunk-5LPUIWNJ.js} +116 -13
- package/dist/chunk-5LPUIWNJ.js.map +1 -0
- package/dist/cli.js +5 -50
- package/dist/cli.js.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/ui/public/app.js +139 -7
- package/dist/ui/public/provider-model-browser.js +27 -8
- package/dist/ui/public/style.css +122 -5
- package/dist/{ui-command-5VEFZ5GM.js → ui-command-DMPYSK7W.js} +74 -5
- package/dist/ui-command-DMPYSK7W.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-LNQ46VW7.js.map +0 -1
- package/dist/ui-command-5VEFZ5GM.js.map +0 -1
package/dist/ui/public/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
formatModelPrice,
|
|
3
3
|
getProviderModelPage,
|
|
4
|
+
isFreeModel,
|
|
4
5
|
PROVIDER_MODEL_PAGE_SIZE,
|
|
5
6
|
} from './provider-model-browser.js';
|
|
6
7
|
import { copyDeviceCode, copyTextToClipboard, oauthConnectionLabel } from './oauth-device.js';
|
|
@@ -24,6 +25,8 @@ const state = {
|
|
|
24
25
|
providerFilter: '',
|
|
25
26
|
activeProviderId: null,
|
|
26
27
|
providerModelFilter: '',
|
|
28
|
+
providerModelMinCtx: 0,
|
|
29
|
+
providerModelFreeOnly: false,
|
|
27
30
|
providerModelPage: 1,
|
|
28
31
|
modelFilter: '',
|
|
29
32
|
modelFreeOnly: false,
|
|
@@ -351,10 +354,6 @@ function fmtCtx(n) {
|
|
|
351
354
|
return String(n);
|
|
352
355
|
}
|
|
353
356
|
|
|
354
|
-
function isFreeModel(model) {
|
|
355
|
-
return Boolean(model?.isFree || model?.freeStatus === 'verified_free' || model?.freeStatus === 'free_provider');
|
|
356
|
-
}
|
|
357
|
-
|
|
358
357
|
function freeBadgeLabel(model) {
|
|
359
358
|
if (model?.freeStatus === 'free_provider') {
|
|
360
359
|
const isCloudflare = model?.providerId === 'cloudflare' || model?.id?.startsWith('@cf/') || model?.id?.startsWith('@hf/');
|
|
@@ -503,7 +502,87 @@ function syncProviderModelBrowserFromHash() {
|
|
|
503
502
|
renderProviderModelBrowser();
|
|
504
503
|
}
|
|
505
504
|
|
|
505
|
+
let activeModelFavPopover = null;
|
|
506
|
+
|
|
507
|
+
function closeModelFavPopover() {
|
|
508
|
+
if (activeModelFavPopover) {
|
|
509
|
+
activeModelFavPopover.remove();
|
|
510
|
+
activeModelFavPopover = null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function toggleModelFavoriteMenu(button, providerId, modelId) {
|
|
515
|
+
const targetKey = `${providerId}::${modelId}`;
|
|
516
|
+
if (activeModelFavPopover && activeModelFavPopover.dataset.targetId === targetKey) {
|
|
517
|
+
closeModelFavPopover();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
closeModelFavPopover();
|
|
521
|
+
|
|
522
|
+
const isGenFav = isGeneralFavorite(providerId, modelId);
|
|
523
|
+
const isAgyFav = state.agyFavorites.some(f => f.providerId === providerId && f.modelId === modelId);
|
|
524
|
+
const genAtCapacity = state.generalFavorites.length >= GENERAL_MAX;
|
|
525
|
+
const agyAtCapacity = state.agyFavorites.length >= AGY_MAX;
|
|
526
|
+
|
|
527
|
+
const popover = document.createElement('div');
|
|
528
|
+
popover.className = 'model-fav-popover';
|
|
529
|
+
popover.dataset.targetId = targetKey;
|
|
530
|
+
|
|
531
|
+
const genDisabled = isGenFav || genAtCapacity;
|
|
532
|
+
let genLabel = '★ Add to Favorites';
|
|
533
|
+
if (isGenFav) genLabel = '✓ In Global Favorites';
|
|
534
|
+
else if (genAtCapacity) genLabel = `★ Favorites full (${GENERAL_MAX}/${GENERAL_MAX})`;
|
|
535
|
+
|
|
536
|
+
const agyDisabled = isAgyFav || agyAtCapacity;
|
|
537
|
+
let agyLabel = '✦ Add to Antigravity Favorites';
|
|
538
|
+
if (isAgyFav) agyLabel = '✓ In Antigravity Favorites';
|
|
539
|
+
else if (agyAtCapacity) agyLabel = `✦ Antigravity full (${AGY_MAX}/${AGY_MAX})`;
|
|
540
|
+
|
|
541
|
+
popover.innerHTML = `
|
|
542
|
+
<button class="model-fav-popover-item ${genDisabled ? 'disabled' : ''}" type="button" data-type="general" ${genDisabled ? 'disabled' : ''}>
|
|
543
|
+
<span>${genLabel}</span>
|
|
544
|
+
<span class="popover-slot-count">${state.generalFavorites.length}/${GENERAL_MAX}</span>
|
|
545
|
+
</button>
|
|
546
|
+
<button class="model-fav-popover-item ${agyDisabled ? 'disabled' : ''}" type="button" data-type="agy" ${agyDisabled ? 'disabled' : ''}>
|
|
547
|
+
<span>${agyLabel}</span>
|
|
548
|
+
<span class="popover-slot-count">${state.agyFavorites.length}/${AGY_MAX}</span>
|
|
549
|
+
</button>
|
|
550
|
+
`;
|
|
551
|
+
|
|
552
|
+
popover.querySelectorAll('.model-fav-popover-item').forEach(item => {
|
|
553
|
+
item.addEventListener('click', event => {
|
|
554
|
+
event.stopPropagation();
|
|
555
|
+
const listType = item.dataset.type;
|
|
556
|
+
addToFavorites({ providerId, modelId }, listType);
|
|
557
|
+
closeModelFavPopover();
|
|
558
|
+
showToast(listType === 'agy' ? 'Added to Antigravity Favorites' : 'Added to Global Favorites');
|
|
559
|
+
renderProviderModelBrowser();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const rect = button.getBoundingClientRect();
|
|
564
|
+
popover.style.position = 'fixed';
|
|
565
|
+
popover.style.top = `${rect.bottom + 4}px`;
|
|
566
|
+
popover.style.left = `${Math.max(10, rect.right - 220)}px`;
|
|
567
|
+
popover.style.zIndex = '9999';
|
|
568
|
+
|
|
569
|
+
document.body.appendChild(popover);
|
|
570
|
+
activeModelFavPopover = popover;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
document.addEventListener('click', event => {
|
|
574
|
+
if (activeModelFavPopover && !activeModelFavPopover.contains(event.target) && !event.target.closest('.btn-model-add')) {
|
|
575
|
+
closeModelFavPopover();
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
document.addEventListener('keydown', event => {
|
|
579
|
+
if (event.key === 'Escape' && activeModelFavPopover) {
|
|
580
|
+
closeModelFavPopover();
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
506
584
|
function renderProviderModelBrowser() {
|
|
585
|
+
closeModelFavPopover();
|
|
507
586
|
const container = document.getElementById('provider-model-browser');
|
|
508
587
|
if (!state.activeProviderId) return;
|
|
509
588
|
|
|
@@ -515,7 +594,11 @@ function renderProviderModelBrowser() {
|
|
|
515
594
|
return;
|
|
516
595
|
}
|
|
517
596
|
|
|
518
|
-
const
|
|
597
|
+
const showActions = !isServerAdminUi();
|
|
598
|
+
const result = getProviderModelPage(provider.models ?? [], state.providerModelFilter, state.providerModelPage, {
|
|
599
|
+
minContextWindow: state.providerModelMinCtx,
|
|
600
|
+
freeOnly: state.providerModelFreeOnly,
|
|
601
|
+
});
|
|
519
602
|
state.providerModelPage = result.page;
|
|
520
603
|
const first = result.total === 0 ? 0 : (result.page - 1) * PROVIDER_MODEL_PAGE_SIZE + 1;
|
|
521
604
|
const last = Math.min(result.page * PROVIDER_MODEL_PAGE_SIZE, result.total);
|
|
@@ -537,19 +620,37 @@ function renderProviderModelBrowser() {
|
|
|
537
620
|
<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>
|
|
538
621
|
<input class="search-input" id="provider-model-search" type="search" value="${escapeHtml(state.providerModelFilter)}" placeholder="Search ${escapeHtml(displayName)} models…" aria-label="Search ${escapeHtml(displayName)} models">
|
|
539
622
|
</div>
|
|
623
|
+
<div class="provider-model-filters">
|
|
624
|
+
<select class="filter-select" id="provider-model-ctx-select" aria-label="Filter by context window size">
|
|
625
|
+
<option value="0" ${state.providerModelMinCtx === 0 ? 'selected' : ''}>All context sizes</option>
|
|
626
|
+
<option value="128000" ${state.providerModelMinCtx === 128000 ? 'selected' : ''}>≥ 128k</option>
|
|
627
|
+
<option value="200000" ${state.providerModelMinCtx === 200000 ? 'selected' : ''}>≥ 200k</option>
|
|
628
|
+
<option value="500000" ${state.providerModelMinCtx === 500000 ? 'selected' : ''}>≥ 500k</option>
|
|
629
|
+
<option value="1000000" ${state.providerModelMinCtx === 1000000 ? 'selected' : ''}>≥ 1M</option>
|
|
630
|
+
</select>
|
|
631
|
+
<label class="filter-checkbox-label">
|
|
632
|
+
<input type="checkbox" id="provider-model-free-checkbox" ${state.providerModelFreeOnly ? 'checked' : ''}>
|
|
633
|
+
<span>Free models only</span>
|
|
634
|
+
</label>
|
|
635
|
+
</div>
|
|
540
636
|
<button class="btn btn-ghost" id="provider-model-refresh" type="button">Refresh</button>
|
|
541
637
|
</div>
|
|
542
638
|
<div class="provider-model-table-wrap">
|
|
543
639
|
<table class="provider-model-table">
|
|
544
|
-
<thead><tr><th>Model</th><th>Context</th><th>Price in / out</th
|
|
640
|
+
<thead><tr><th>Model</th><th>Context</th><th>Price in / out</th>${showActions ? '<th class="th-actions">Actions</th>' : ''}</tr></thead>
|
|
545
641
|
<tbody>
|
|
546
642
|
${result.items.map(model => `
|
|
547
643
|
<tr>
|
|
548
644
|
<td><strong>${escapeHtml(model.name || model.id)}</strong><code>${escapeHtml(model.id)}</code></td>
|
|
549
645
|
<td>${fmtCtx(model.contextWindow) || '—'}</td>
|
|
550
646
|
<td>${formatModelPrice(model.cost, isFreeModel(model), freeBadgeLabel(model))}</td>
|
|
647
|
+
${showActions ? `
|
|
648
|
+
<td class="td-actions">
|
|
649
|
+
<button class="btn-model-add" type="button" data-model-id="${escapeHtml(model.id)}" title="Add to favorites">+</button>
|
|
650
|
+
</td>
|
|
651
|
+
` : ''}
|
|
551
652
|
</tr>
|
|
552
|
-
`).join('') ||
|
|
653
|
+
`).join('') || `<tr><td colspan="${showActions ? 4 : 3}" class="provider-model-empty">No models match your search and filters.</td></tr>`}
|
|
553
654
|
</tbody>
|
|
554
655
|
</table>
|
|
555
656
|
</div>
|
|
@@ -571,6 +672,25 @@ function renderProviderModelBrowser() {
|
|
|
571
672
|
search?.focus();
|
|
572
673
|
search?.setSelectionRange(search.value.length, search.value.length);
|
|
573
674
|
});
|
|
675
|
+
document.getElementById('provider-model-ctx-select')?.addEventListener('change', event => {
|
|
676
|
+
state.providerModelMinCtx = parseInt(event.target.value, 10) || 0;
|
|
677
|
+
state.providerModelPage = 1;
|
|
678
|
+
renderProviderModelBrowser();
|
|
679
|
+
});
|
|
680
|
+
document.getElementById('provider-model-free-checkbox')?.addEventListener('change', event => {
|
|
681
|
+
state.providerModelFreeOnly = event.target.checked;
|
|
682
|
+
state.providerModelPage = 1;
|
|
683
|
+
renderProviderModelBrowser();
|
|
684
|
+
});
|
|
685
|
+
if (showActions) {
|
|
686
|
+
container.querySelectorAll('.btn-model-add').forEach(btn => {
|
|
687
|
+
btn.addEventListener('click', event => {
|
|
688
|
+
event.stopPropagation();
|
|
689
|
+
const modelId = btn.dataset.modelId;
|
|
690
|
+
toggleModelFavoriteMenu(btn, provider.id, modelId);
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
}
|
|
574
694
|
document.getElementById('provider-model-prev').addEventListener('click', () => {
|
|
575
695
|
state.providerModelPage -= 1;
|
|
576
696
|
renderProviderModelBrowser();
|
|
@@ -2336,6 +2456,7 @@ function seedServerFormFromSaved(saved) {
|
|
|
2336
2456
|
f.freeModelsOnly = Boolean(saved.freeModelsOnly);
|
|
2337
2457
|
f.exposedProviders = saved.exposedProviders ? [...saved.exposedProviders] : [];
|
|
2338
2458
|
f.maskGatewayIds = saved.maskGatewayIds;
|
|
2459
|
+
f.autostart = Boolean(saved.autostart);
|
|
2339
2460
|
// Published Docker ports only reach 0.0.0.0 inside the container.
|
|
2340
2461
|
f.listenMode = isServerAdminUi() || saved.listenMode === 'network' ? 'network' : 'local';
|
|
2341
2462
|
// Env password (Docker): prefill the field so Start works and clients can copy it.
|
|
@@ -2530,6 +2651,10 @@ function renderServerSetup(s) {
|
|
|
2530
2651
|
<input type="checkbox" ${f.freeModelsOnly ? 'checked' : ''} onchange="setServerFreeModelsOnly(this.checked)">
|
|
2531
2652
|
<span>Free models only <span class="server-field-hint">(includes verified $0 models and free developer access)</span></span>
|
|
2532
2653
|
</label>
|
|
2654
|
+
<label class="server-toggle-row">
|
|
2655
|
+
<input type="checkbox" ${f.autostart ? 'checked' : ''} onchange="setServerAutostart(this.checked)">
|
|
2656
|
+
<span>Auto-start API Gateway when relay-ai starts <span class="server-field-hint">(port 17645 auto-starts on service boot)</span></span>
|
|
2657
|
+
</label>
|
|
2533
2658
|
<div class="server-field-hint">${escapeHtml(freeModelsHint)}</div>
|
|
2534
2659
|
</div>
|
|
2535
2660
|
|
|
@@ -2695,6 +2820,12 @@ function setServerFreeModelsOnly(checked) {
|
|
|
2695
2820
|
renderServerPanel();
|
|
2696
2821
|
}
|
|
2697
2822
|
|
|
2823
|
+
function setServerAutostart(checked) {
|
|
2824
|
+
state.server.form.autostart = checked;
|
|
2825
|
+
api('POST', '/api/server/config', { autostart: checked }).catch(() => {});
|
|
2826
|
+
renderServerPanel();
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2698
2829
|
function setServerListenMode(mode) {
|
|
2699
2830
|
state.server.form.listenMode = mode;
|
|
2700
2831
|
state.server.error = null;
|
|
@@ -2735,6 +2866,7 @@ async function submitServerStart() {
|
|
|
2735
2866
|
: null,
|
|
2736
2867
|
maskGatewayIds: f.maskGatewayIds,
|
|
2737
2868
|
listenMode: f.listenMode,
|
|
2869
|
+
autostart: f.autostart,
|
|
2738
2870
|
passwordMode: f.passwordMode,
|
|
2739
2871
|
password: f.password,
|
|
2740
2872
|
savePassword: f.savePassword,
|
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
export const PROVIDER_MODEL_PAGE_SIZE = 25;
|
|
2
2
|
|
|
3
|
-
export function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
model
|
|
8
|
-
|| (model.
|
|
3
|
+
export function isFreeModel(model) {
|
|
4
|
+
return Boolean(
|
|
5
|
+
model?.isFree
|
|
6
|
+
|| model?.freeStatus === 'verified_free'
|
|
7
|
+
|| model?.freeStatus === 'free_provider'
|
|
8
|
+
|| (model?.cost && model?.cost.input === 0 && model?.cost.output === 0),
|
|
9
9
|
);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function
|
|
13
|
-
const
|
|
12
|
+
export function filterProviderModels(models, query, opts = {}) {
|
|
13
|
+
const needle = query.trim().toLowerCase();
|
|
14
|
+
const minCtx = opts.minContextWindow ?? 0;
|
|
15
|
+
const freeOnly = Boolean(opts.freeOnly);
|
|
16
|
+
|
|
17
|
+
return models.filter(model => {
|
|
18
|
+
if (needle && !model.id.toLowerCase().includes(needle) && !(model.name ?? '').toLowerCase().includes(needle)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (minCtx > 0 && (model.contextWindow ?? 0) < minCtx) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (freeOnly && !isFreeModel(model)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getProviderModelPage(models, query, requestedPage, opts = {}) {
|
|
32
|
+
const filtered = filterProviderModels(models, query, opts);
|
|
14
33
|
const totalPages = Math.max(1, Math.ceil(filtered.length / PROVIDER_MODEL_PAGE_SIZE));
|
|
15
34
|
const page = Math.min(Math.max(1, requestedPage), totalPages);
|
|
16
35
|
const start = (page - 1) * PROVIDER_MODEL_PAGE_SIZE;
|
package/dist/ui/public/style.css
CHANGED
|
@@ -1194,12 +1194,55 @@ input { font-family: inherit; }
|
|
|
1194
1194
|
.provider-browser-hero .section-heading { margin-bottom: 4px; }
|
|
1195
1195
|
|
|
1196
1196
|
.provider-browser-tools {
|
|
1197
|
-
display:
|
|
1198
|
-
|
|
1199
|
-
|
|
1197
|
+
display: flex;
|
|
1198
|
+
flex-wrap: wrap;
|
|
1199
|
+
align-items: center;
|
|
1200
|
+
gap: 12px;
|
|
1200
1201
|
margin-bottom: 14px;
|
|
1201
1202
|
}
|
|
1202
1203
|
|
|
1204
|
+
.provider-browser-tools .search-field {
|
|
1205
|
+
flex: 1;
|
|
1206
|
+
min-width: 200px;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
.provider-model-filters {
|
|
1210
|
+
display: flex;
|
|
1211
|
+
align-items: center;
|
|
1212
|
+
gap: 12px;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
.filter-select {
|
|
1216
|
+
padding: 7px 12px;
|
|
1217
|
+
font-size: 13px;
|
|
1218
|
+
color: var(--text-1);
|
|
1219
|
+
background: var(--surface);
|
|
1220
|
+
border: 1px solid var(--border);
|
|
1221
|
+
border-radius: var(--radius);
|
|
1222
|
+
outline: none;
|
|
1223
|
+
cursor: pointer;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
.filter-select:focus {
|
|
1227
|
+
border-color: var(--accent, #00d2b8);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.filter-checkbox-label {
|
|
1231
|
+
display: flex;
|
|
1232
|
+
align-items: center;
|
|
1233
|
+
gap: 6px;
|
|
1234
|
+
font-size: 13px;
|
|
1235
|
+
color: var(--text-2);
|
|
1236
|
+
cursor: pointer;
|
|
1237
|
+
user-select: none;
|
|
1238
|
+
white-space: nowrap;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
.filter-checkbox-label input {
|
|
1242
|
+
accent-color: var(--accent, #00d2b8);
|
|
1243
|
+
cursor: pointer;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1203
1246
|
.provider-model-table-wrap {
|
|
1204
1247
|
overflow-x: auto;
|
|
1205
1248
|
border: 1px solid var(--border);
|
|
@@ -1225,8 +1268,82 @@ input { font-family: inherit; }
|
|
|
1225
1268
|
text-transform: uppercase;
|
|
1226
1269
|
}
|
|
1227
1270
|
|
|
1228
|
-
.provider-model-table th:nth-child(2) { width:
|
|
1229
|
-
.provider-model-table th:nth-child(3) { width:
|
|
1271
|
+
.provider-model-table th:nth-child(2) { width: 110px; }
|
|
1272
|
+
.provider-model-table th:nth-child(3) { width: 160px; }
|
|
1273
|
+
.provider-model-table th.th-actions { width: 80px; text-align: center; }
|
|
1274
|
+
.provider-model-table td.td-actions { text-align: center; }
|
|
1275
|
+
|
|
1276
|
+
.btn-model-add {
|
|
1277
|
+
display: inline-flex;
|
|
1278
|
+
align-items: center;
|
|
1279
|
+
justify-content: center;
|
|
1280
|
+
width: 28px;
|
|
1281
|
+
height: 28px;
|
|
1282
|
+
border-radius: 6px;
|
|
1283
|
+
border: 1px solid var(--border);
|
|
1284
|
+
background: var(--surface-2);
|
|
1285
|
+
color: var(--text-1);
|
|
1286
|
+
font-size: 16px;
|
|
1287
|
+
font-weight: 600;
|
|
1288
|
+
cursor: pointer;
|
|
1289
|
+
transition: all 0.15s ease;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
.btn-model-add:hover {
|
|
1293
|
+
background: var(--surface-hover);
|
|
1294
|
+
border-color: var(--accent, #00d2b8);
|
|
1295
|
+
color: var(--accent, #00d2b8);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.model-fav-popover {
|
|
1299
|
+
background: var(--surface);
|
|
1300
|
+
border: 1px solid var(--border);
|
|
1301
|
+
border-radius: 8px;
|
|
1302
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
|
|
1303
|
+
padding: 5px;
|
|
1304
|
+
width: 230px;
|
|
1305
|
+
display: flex;
|
|
1306
|
+
flex-direction: column;
|
|
1307
|
+
gap: 2px;
|
|
1308
|
+
animation: popoverFadeIn 0.12s ease-out;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
@keyframes popoverFadeIn {
|
|
1312
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
1313
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
.model-fav-popover-item {
|
|
1317
|
+
display: flex;
|
|
1318
|
+
align-items: center;
|
|
1319
|
+
justify-content: space-between;
|
|
1320
|
+
padding: 8px 10px;
|
|
1321
|
+
border-radius: 5px;
|
|
1322
|
+
border: 0;
|
|
1323
|
+
background: transparent;
|
|
1324
|
+
color: var(--text-1);
|
|
1325
|
+
font-size: 12px;
|
|
1326
|
+
font-weight: 500;
|
|
1327
|
+
cursor: pointer;
|
|
1328
|
+
text-align: left;
|
|
1329
|
+
transition: background 0.12s ease, color 0.12s ease;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.model-fav-popover-item:hover:not(.disabled) {
|
|
1333
|
+
background: var(--surface-hover);
|
|
1334
|
+
color: var(--accent, #00d2b8);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
.model-fav-popover-item.disabled {
|
|
1338
|
+
opacity: 0.5;
|
|
1339
|
+
cursor: not-allowed;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
.popover-slot-count {
|
|
1343
|
+
font-size: 11px;
|
|
1344
|
+
opacity: 0.7;
|
|
1345
|
+
margin-left: 6px;
|
|
1346
|
+
}
|
|
1230
1347
|
|
|
1231
1348
|
.provider-model-table td {
|
|
1232
1349
|
padding: 12px 16px;
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
resolveAdvertiseAddresses,
|
|
59
59
|
resolveAdvertiseGatewayPort,
|
|
60
60
|
resolveProviderCredential,
|
|
61
|
+
resolveServerAutostart,
|
|
61
62
|
resolveServerUpstreamApiKey,
|
|
62
63
|
saveNativeOAuthCredential,
|
|
63
64
|
savePreferences,
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
sendJson,
|
|
66
67
|
setAppPathOverride,
|
|
67
68
|
setSavedServerPassword,
|
|
69
|
+
setServerAutostart,
|
|
68
70
|
setServerExposedProviders,
|
|
69
71
|
setServerFavoritesOnly,
|
|
70
72
|
setServerFreeModelsOnly,
|
|
@@ -75,7 +77,7 @@ import {
|
|
|
75
77
|
supportsClaudeTransparentMode,
|
|
76
78
|
validateCustomEndpointUrl,
|
|
77
79
|
writeSecureLogLine
|
|
78
|
-
} from "./chunk-
|
|
80
|
+
} from "./chunk-5LPUIWNJ.js";
|
|
79
81
|
import {
|
|
80
82
|
__toCommonJS,
|
|
81
83
|
init_provider_templates,
|
|
@@ -397,6 +399,7 @@ async function buildSavedConfig() {
|
|
|
397
399
|
exposedProviders: getServerExposedProviders(),
|
|
398
400
|
maskGatewayIds: getServerMaskGatewayIds(),
|
|
399
401
|
listenMode: getServerListenMode(),
|
|
402
|
+
autostart: resolveServerAutostart(),
|
|
400
403
|
hasSavedPassword: await hasSavedPasswordCached(),
|
|
401
404
|
hasEnvPassword: Boolean(envPassword),
|
|
402
405
|
...envPassword ? { prefillPassword: envPassword } : {}
|
|
@@ -509,6 +512,9 @@ async function doStartGatewayServer(req, opts) {
|
|
|
509
512
|
}
|
|
510
513
|
setServerMaskGatewayIds(req.maskGatewayIds);
|
|
511
514
|
setServerListenMode(req.listenMode);
|
|
515
|
+
if (req.autostart !== void 0) {
|
|
516
|
+
setServerAutostart(req.autostart);
|
|
517
|
+
}
|
|
512
518
|
const host = req.listenMode === "network" ? "0.0.0.0" : "127.0.0.1";
|
|
513
519
|
const gateway = req.maskGatewayIds ? { maskGatewayIds: true } : void 0;
|
|
514
520
|
let handle;
|
|
@@ -536,7 +542,8 @@ async function doStartGatewayServer(req, opts) {
|
|
|
536
542
|
freeModelsOnly: req.freeModelsOnly,
|
|
537
543
|
exposedProviders: req.exposedProviders,
|
|
538
544
|
maskGatewayIds: req.maskGatewayIds,
|
|
539
|
-
listenMode: req.listenMode
|
|
545
|
+
listenMode: req.listenMode,
|
|
546
|
+
autostart: req.autostart ?? resolveServerAutostart()
|
|
540
547
|
},
|
|
541
548
|
providerSummary: summarizeServerProviders(models),
|
|
542
549
|
modelRows: buildModelRows(models, gateway)
|
|
@@ -551,6 +558,23 @@ async function stopGatewayServer() {
|
|
|
551
558
|
}
|
|
552
559
|
return { ok: true, stopped: false };
|
|
553
560
|
}
|
|
561
|
+
async function startGatewayServerFromSavedConfig(opts) {
|
|
562
|
+
const req = {
|
|
563
|
+
favoritesOnly: getServerFavoritesOnly(),
|
|
564
|
+
freeModelsOnly: getServerFreeModelsOnly(),
|
|
565
|
+
exposedProviders: getServerExposedProviders(),
|
|
566
|
+
maskGatewayIds: getServerMaskGatewayIds(),
|
|
567
|
+
listenMode: getServerListenMode(),
|
|
568
|
+
autostart: resolveServerAutostart(),
|
|
569
|
+
passwordMode: "saved"
|
|
570
|
+
};
|
|
571
|
+
const res = await startGatewayServer(req, opts);
|
|
572
|
+
if (res.ok) {
|
|
573
|
+
return res.status;
|
|
574
|
+
}
|
|
575
|
+
console.warn(`[server-autostart] Warning: ${res.error}`);
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
554
578
|
|
|
555
579
|
// src/ui/api.ts
|
|
556
580
|
var execAsync = promisify(exec);
|
|
@@ -650,6 +674,8 @@ function handleUiApiRequest(req, res, opts = {}) {
|
|
|
650
674
|
handleStartServer(req, res, opts);
|
|
651
675
|
} else if (url === "/api/server/stop" && req.method === "POST") {
|
|
652
676
|
handleStopServer(res, opts);
|
|
677
|
+
} else if (url === "/api/server/config" && req.method === "POST") {
|
|
678
|
+
handlePostServerConfig(req, res);
|
|
653
679
|
} else {
|
|
654
680
|
sendJson(res, 404, { error: "Not found" });
|
|
655
681
|
}
|
|
@@ -1284,6 +1310,7 @@ async function handleStartServer(req, res, opts) {
|
|
|
1284
1310
|
exposedProviders: Array.isArray(body.exposedProviders) ? body.exposedProviders : null,
|
|
1285
1311
|
maskGatewayIds: body.maskGatewayIds,
|
|
1286
1312
|
listenMode,
|
|
1313
|
+
autostart: typeof body.autostart === "boolean" ? body.autostart : void 0,
|
|
1287
1314
|
passwordMode: body.passwordMode === "saved" ? "saved" : "new",
|
|
1288
1315
|
password: typeof body.password === "string" ? body.password : void 0,
|
|
1289
1316
|
savePassword: Boolean(body.savePassword)
|
|
@@ -1303,6 +1330,17 @@ async function handleStartServer(req, res, opts) {
|
|
|
1303
1330
|
sendJson(res, 500, { ok: false, error: String(err) });
|
|
1304
1331
|
}
|
|
1305
1332
|
}
|
|
1333
|
+
async function handlePostServerConfig(req, res) {
|
|
1334
|
+
try {
|
|
1335
|
+
const body = JSON.parse(await readBody(req));
|
|
1336
|
+
if (typeof body.autostart === "boolean") {
|
|
1337
|
+
setServerAutostart(body.autostart);
|
|
1338
|
+
}
|
|
1339
|
+
sendJson(res, 200, { ok: true });
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
sendJson(res, 400, { error: String(err) });
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1306
1344
|
async function handleStopServer(res, opts) {
|
|
1307
1345
|
try {
|
|
1308
1346
|
const result = await stopGatewayServer();
|
|
@@ -1454,11 +1492,35 @@ function removeLock() {
|
|
|
1454
1492
|
} catch {
|
|
1455
1493
|
}
|
|
1456
1494
|
}
|
|
1457
|
-
function
|
|
1495
|
+
async function probeServerHealth(port) {
|
|
1496
|
+
try {
|
|
1497
|
+
const controller = new AbortController();
|
|
1498
|
+
const timer = setTimeout(() => controller.abort(), 500);
|
|
1499
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/config`, { signal: controller.signal });
|
|
1500
|
+
clearTimeout(timer);
|
|
1501
|
+
if (res.status === 200) {
|
|
1502
|
+
const data = await res.json().catch(() => null);
|
|
1503
|
+
if (data && typeof data === "object") return true;
|
|
1504
|
+
}
|
|
1505
|
+
return false;
|
|
1506
|
+
} catch {
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
async function checkExistingServer() {
|
|
1458
1511
|
if (!existsSync3(LOCK_FILE)) return null;
|
|
1459
1512
|
try {
|
|
1460
1513
|
const { pid, port } = JSON.parse(readFileSync(LOCK_FILE, "utf8"));
|
|
1514
|
+
if (pid === process.pid) {
|
|
1515
|
+
removeLock();
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1461
1518
|
process.kill(pid, 0);
|
|
1519
|
+
const isAlive = await probeServerHealth(port ?? DEFAULT_SERVER_UI_PORT);
|
|
1520
|
+
if (!isAlive) {
|
|
1521
|
+
removeLock();
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1462
1524
|
return `http://127.0.0.1:${port}`;
|
|
1463
1525
|
} catch {
|
|
1464
1526
|
removeLock();
|
|
@@ -1487,7 +1549,7 @@ async function resolveUiShutdownDecision(signal, promptClose = () => p.confirm({
|
|
|
1487
1549
|
async function runUiCommand(opts = {}) {
|
|
1488
1550
|
const runtime = resolveUiRuntimeConfig(opts);
|
|
1489
1551
|
await ensureOpencodeCloudProviders();
|
|
1490
|
-
const existing = checkExistingServer();
|
|
1552
|
+
const existing = await checkExistingServer();
|
|
1491
1553
|
if (existing) {
|
|
1492
1554
|
console.log(`
|
|
1493
1555
|
${pc.bold("relay-ai UI")} already running at ${pc.cyan(existing)}
|
|
@@ -1542,6 +1604,11 @@ async function runUiCommand(opts = {}) {
|
|
|
1542
1604
|
const url = `http://${displayHost}:${port}`;
|
|
1543
1605
|
mkdirSync(getAppHome(), { recursive: true });
|
|
1544
1606
|
writeFileSync2(LOCK_FILE, JSON.stringify({ pid: process.pid, port, mode: runtime.mode }));
|
|
1607
|
+
if (resolveServerAutostart()) {
|
|
1608
|
+
startGatewayServerFromSavedConfig().catch((err) => {
|
|
1609
|
+
trace?.(`gateway autostart error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1545
1612
|
const cleanup = () => {
|
|
1546
1613
|
removeLock();
|
|
1547
1614
|
server.close();
|
|
@@ -1596,11 +1663,13 @@ async function runUiCommand(opts = {}) {
|
|
|
1596
1663
|
return 0;
|
|
1597
1664
|
}
|
|
1598
1665
|
export {
|
|
1666
|
+
checkExistingServer,
|
|
1599
1667
|
formatUiServerLifecycleMessage,
|
|
1600
1668
|
isUiApiRoute,
|
|
1669
|
+
probeServerHealth,
|
|
1601
1670
|
resolveUiMode,
|
|
1602
1671
|
resolveUiRuntimeConfig,
|
|
1603
1672
|
resolveUiShutdownDecision,
|
|
1604
1673
|
runUiCommand
|
|
1605
1674
|
};
|
|
1606
|
-
//# sourceMappingURL=ui-command-
|
|
1675
|
+
//# sourceMappingURL=ui-command-DMPYSK7W.js.map
|