Improve Ollama setup and model endpoint handling

This commit is contained in:
pewdiepie-archdaemon
2026-06-01 10:00:15 +09:00
parent 051751adcd
commit fc7f107b22
22 changed files with 982 additions and 131 deletions

View File

@@ -280,6 +280,51 @@ function _isLocalEndpoint(url) {
} catch { return false; }
}
async function _refreshAfterEndpointChange(deletedEndpointId) {
try {
const sm = window.sessionModule;
const pending = sm && sm.getPendingChat ? sm.getPendingChat() : null;
if (deletedEndpointId && pending && String(pending.endpointId || '') === String(deletedEndpointId)) {
if (sm.setPendingChat) sm.setPendingChat(null);
}
} catch (_) {}
try {
if (window.modelsModule && window.modelsModule.refreshModels) {
await window.modelsModule.refreshModels(true);
}
} catch (_) {}
try {
window.dispatchEvent(new CustomEvent('ge:model-endpoints-updated', {
detail: { deletedEndpointId: deletedEndpointId || null }
}));
} catch (_) {}
try {
if (window.sessionModule && window.sessionModule.updateModelPicker) {
window.sessionModule.updateModelPicker();
}
} catch (_) {}
}
async function _selectAddedModelInChat(endpoint) {
const modelId = endpoint && Array.isArray(endpoint.models) ? endpoint.models[0] : '';
if (!modelId) return;
try {
if (window.modelsModule && window.modelsModule.refreshModels) {
await window.modelsModule.refreshModels(true);
}
} catch (_) {}
try {
document.dispatchEvent(new CustomEvent('odysseus:auto-select-model', {
detail: {
endpointId: endpoint.id || '',
endpointName: endpoint.name || '',
modelId,
url: endpoint.base_url || '',
}
}));
} catch (_) {}
}
async function loadEndpoints() {
const listLocal = el('adm-epList-local');
const listApi = el('adm-epList-api');
@@ -306,7 +351,7 @@ async function loadEndpoints() {
try { data = await res.json(); } catch { data = []; }
}
if (!Array.isArray(data) || data.length === 0) {
const empty = '<div class="admin-empty">No endpoints configured</div>';
const empty = '<div class="admin-empty">None</div>';
if (listLocal) listLocal.innerHTML = empty;
if (listApi) listApi.innerHTML = '<div class="admin-empty">None</div>';
if (listLegacy) listLegacy.innerHTML = empty;
@@ -319,9 +364,11 @@ async function loadEndpoints() {
// empty, but we still need to render the expand panel so the user can
// un-hide them. Gate on the total instead.
const hasModels = ep.online && totalCount > 0;
const statusBadge = ep.online
? `<span class="admin-badge">${visibleCount}/${totalCount} models enabled</span>`
: '<span class="admin-badge admin-badge-off">offline</span>';
const statusBadge = ep.status === 'empty'
? '<span class="admin-badge">no models</span>'
: ep.online
? `<span class="admin-badge">${visibleCount}/${totalCount} models enabled</span>`
: '<span class="admin-badge admin-badge-off">offline</span>';
const justAddedClass = (_recentlyAddedEpId && String(ep.id) === _recentlyAddedEpId) ? ' adm-ep-just-added' : '';
return `
<div class="admin-user-row${ep.is_enabled ? '' : ' admin-ep-disabled'}${justAddedClass}" data-adm-ep-id="${ep.id}">
@@ -417,7 +464,10 @@ async function loadEndpoints() {
// Optimistic: remove from UI immediately
const row = btn.closest('[data-adm-ep-id]');
if (row) row.remove();
fetch('/api/model-endpoints/' + epId, { method: 'DELETE' }).then(() => loadEndpoints()).catch(() => loadEndpoints());
fetch('/api/model-endpoints/' + epId, { method: 'DELETE' })
.then(() => _refreshAfterEndpointChange(epId))
.then(() => loadEndpoints())
.catch(() => loadEndpoints());
});
});
// Clear the just-added marker now that the row has been rendered
@@ -571,6 +621,7 @@ function initEndpointForm() {
if (picker && pickerBtn && pickerMenu && pickerCurrent) {
_renderPickerMenu();
_syncPickerCurrent();
if (provider.value && !urlInput.value) urlInput.value = provider.value;
pickerBtn.addEventListener('click', (e) => {
e.stopPropagation();
pickerMenu.classList.toggle('hidden');
@@ -593,6 +644,13 @@ function initEndpointForm() {
if (provider.value) urlInput.value = provider.value;
else urlInput.value = '';
});
urlInput.addEventListener('input', () => {
if (provider.value && urlInput.value.trim() !== provider.value) {
provider.value = '';
_renderPickerMenu();
_syncPickerCurrent();
}
});
function _normalizeBaseUrl(raw) {
let u = raw.trim();
// Fix common protocol typos
@@ -623,15 +681,96 @@ function initEndpointForm() {
return u;
}
async function _defaultOllamaUrl() {
try {
const res = await fetch('/api/runtime', { credentials: 'same-origin' });
if (res.ok) {
const data = await res.json();
if (data && data.ollama_base_url) return data.ollama_base_url;
}
} catch (_) {}
return 'http://127.0.0.1:11434/v1';
}
function _renderEndpointTestResult(msg, res, d) {
if (res.ok && d.status === 'empty') {
msg.textContent = 'Online — no models found';
msg.className = 'admin-success';
return;
}
if (res.ok && d.online) {
const models = d.models || [];
const preview = models.slice(0, 3).map(m => esc(String(m).split('/').pop())).join(', ');
msg.innerHTML = `Online — found ${models.length} model${models.length !== 1 ? 's' : ''}${preview ? `: ${preview}${models.length > 3 ? ', …' : ''}` : ''}`;
msg.className = 'admin-success';
return;
}
msg.textContent = (d && d.detail) || (d && d.ping_error ? `Offline — ${d.ping_error}` : 'Offline');
msg.className = 'admin-error';
}
function _endpointMsg(kind) {
return el(kind === 'local' ? 'adm-epLocalMsg' : 'adm-epApiMsg') || el('adm-epMsg');
}
let apiTestController = null;
const apiTestBtn = el('adm-epApiTestBtn');
const apiCancelTestBtn = el('adm-epApiCancelTestBtn');
if (apiTestBtn) {
apiTestBtn.addEventListener('click', async () => {
const msg = _endpointMsg('api');
msg.textContent = ''; msg.className = '';
const rawUrl = (urlInput.value || provider.value).trim();
const apiKey = el('adm-epApiKey').value.trim();
if (!rawUrl) { msg.textContent = 'Select a provider or enter a base URL'; msg.className = 'admin-error'; return; }
if (provider.value && !apiKey) { msg.textContent = 'API key is required for cloud providers'; msg.className = 'admin-error'; return; }
const url = provider.value && rawUrl === provider.value ? rawUrl : _normalizeBaseUrl(rawUrl);
apiTestController = new AbortController();
apiTestBtn.disabled = true;
apiTestBtn.textContent = 'Testing...';
if (apiCancelTestBtn) apiCancelTestBtn.classList.remove('hidden');
try {
const fd = new FormData();
fd.append('base_url', url);
if (apiKey) fd.append('api_key', apiKey);
const res = await fetch('/api/model-endpoints/test', {
method: 'POST',
body: fd,
credentials: 'same-origin',
signal: apiTestController.signal,
});
const d = await res.json();
_renderEndpointTestResult(msg, res, d);
} catch (e) {
if (e && e.name === 'AbortError') {
msg.textContent = 'Test canceled';
msg.className = '';
} else {
msg.textContent = 'Test failed: ' + (e && e.message ? e.message : 'request failed');
msg.className = 'admin-error';
}
}
apiTestController = null;
apiTestBtn.disabled = false;
apiTestBtn.textContent = 'Test';
if (apiCancelTestBtn) apiCancelTestBtn.classList.add('hidden');
});
}
if (apiCancelTestBtn) {
apiCancelTestBtn.addEventListener('click', () => {
if (apiTestController) apiTestController.abort();
});
}
el('adm-epAddBtn').addEventListener('click', async () => {
const msg = el('adm-epMsg');
const msg = _endpointMsg('api');
msg.textContent = ''; msg.className = '';
const rawUrl = (provider.value || urlInput.value).trim();
const rawUrl = (urlInput.value || provider.value).trim();
const apiKey = el('adm-epApiKey').value.trim();
if (!rawUrl) { msg.textContent = 'Select a provider or enter a base URL'; msg.className = 'admin-error'; return; }
if (provider.value && !apiKey) { msg.textContent = 'API key is required for cloud providers'; msg.className = 'admin-error'; return; }
// Normalize URL (fix typos, add /v1, strip wrong paths)
const url = provider.value ? rawUrl : _normalizeBaseUrl(rawUrl);
const url = provider.value && rawUrl === provider.value ? rawUrl : _normalizeBaseUrl(rawUrl);
const btn = el('adm-epAddBtn');
btn.disabled = true; btn.textContent = 'Adding...';
try {
@@ -640,7 +779,7 @@ function initEndpointForm() {
if (apiKey) fd.append('api_key', apiKey);
const epType = el('adm-epType');
if (epType) fd.append('model_type', epType.value);
fd.append('skip_probe', 'true');
fd.append('skip_probe', 'false');
const res = await fetch('/api/model-endpoints', { method: 'POST', body: fd, credentials: 'same-origin' });
const d = await res.json();
if (res.ok) {
@@ -649,45 +788,17 @@ function initEndpointForm() {
el('adm-epApiKey').value = ''; provider.value = '';
if (epType) epType.value = 'llm';
if (d.id) _recentlyAddedEpId = String(d.id);
loadEndpoints();
await loadEndpoints();
await _selectAddedModelInChat(d);
if (!d.online) {
msg.textContent = 'Added (endpoint offline — will retry on next load)';
msg.className = 'admin-error';
} else {
msg.innerHTML = `Added — found ${count} model${count !== 1 ? 's' : ''}. `
+ `<a href="#" id="adm-probe-now" style="text-decoration:underline;cursor:pointer;">Probe models?</a>`;
} else if (d.status === 'empty') {
msg.textContent = 'Added — endpoint reachable, no models found';
msg.className = 'admin-success';
} else {
msg.textContent = `Added — found ${count} model${count !== 1 ? 's' : ''}`;
msg.className = 'admin-success';
const probeLink = el('adm-probe-now');
if (probeLink) {
probeLink.addEventListener('click', async (e) => {
e.preventDefault();
msg.textContent = 'Probing models...';
try {
const es = new EventSource(`/api/model-endpoints/${d.id}/probe`);
let lines = [];
es.onmessage = (ev) => {
const r = JSON.parse(ev.data);
if (r.type === 'probe_result') {
const dot = r.status === 'ok' ? '<span style="color:var(--color-success);">●</span>'
: r.status === 'timeout' ? '<span style="color:var(--color-warning);">●</span>'
: '<span style="color:var(--color-error);">●</span>';
const lat = r.latency_ms ? ` ${r.latency_ms}ms` : '';
const err = r.error ? `${esc(r.error)}` : '';
lines.push(`${dot} ${esc(r.model.split('/').pop())}${lat}${err}`);
msg.innerHTML = `Probing... ${lines.length} checked<div style="font-size:0.78rem;margin-top:4px;">${lines.join('<br>')}</div>`;
} else if (r.type === 'probe_done') {
es.close();
let txt = `Done — ${r.ok}/${r.ok + r.hidden} models responding`;
if (r.hidden) txt += `${r.hidden} non-responding hidden`;
txt += `<div style="font-size:0.78rem;margin-top:4px;">${lines.join('<br>')}</div>`;
msg.innerHTML = txt;
loadEndpoints();
}
};
es.onerror = () => { es.close(); msg.textContent += ' (probe connection lost)'; };
} catch (e) { msg.textContent = 'Probe failed'; msg.className = 'admin-error'; }
});
}
}
} else { msg.textContent = d.detail || 'Failed'; msg.className = 'admin-error'; }
} catch (e) { msg.textContent = 'Request failed'; msg.className = 'admin-error'; }
@@ -696,9 +807,33 @@ function initEndpointForm() {
// Local "Add" button — sibling form for self-hosted base URLs.
const localAddBtn = el('adm-epLocalAddBtn');
const localTestBtn = el('adm-epLocalTestBtn');
if (localTestBtn) {
localTestBtn.addEventListener('click', async () => {
const msg = _endpointMsg('local');
msg.textContent = ''; msg.className = '';
const raw = (el('adm-epLocalUrl').value || '').trim();
if (!raw) { msg.textContent = 'Enter a base URL to test'; msg.className = 'admin-error'; return; }
const url = _normalizeBaseUrl(raw);
localTestBtn.disabled = true;
localTestBtn.textContent = 'Testing...';
try {
const fd = new FormData();
fd.append('base_url', url);
const res = await fetch('/api/model-endpoints/test', { method: 'POST', body: fd, credentials: 'same-origin' });
const d = await res.json();
_renderEndpointTestResult(msg, res, d);
} catch (e) {
msg.textContent = 'Test failed: ' + (e && e.message ? e.message : 'request failed');
msg.className = 'admin-error';
}
localTestBtn.disabled = false;
localTestBtn.textContent = 'Test';
});
}
if (localAddBtn) {
localAddBtn.addEventListener('click', async () => {
const msg = el('adm-epMsg');
const msg = _endpointMsg('local');
msg.textContent = ''; msg.className = '';
const raw = (el('adm-epLocalUrl').value || '').trim();
if (!raw) { msg.textContent = 'Enter a base URL (e.g. http://localhost:8002/v1)'; msg.className = 'admin-error'; return; }
@@ -709,16 +844,19 @@ function initEndpointForm() {
fd.append('base_url', url);
const lt = el('adm-epLocalType');
if (lt) fd.append('model_type', lt.value);
fd.append('skip_probe', 'true');
fd.append('skip_probe', 'false');
const res = await fetch('/api/model-endpoints', { method: 'POST', body: fd, credentials: 'same-origin' });
const d = await res.json();
if (res.ok) {
el('adm-epLocalUrl').value = '';
if (lt) lt.value = 'llm';
if (d.id) _recentlyAddedEpId = String(d.id);
loadEndpoints();
await loadEndpoints();
await _selectAddedModelInChat(d);
const count = (d.models || []).length;
msg.textContent = d.online
msg.textContent = d.status === 'empty'
? 'Added — Ollama is running, no models pulled yet'
: d.online
? `Added — found ${count} model${count !== 1 ? 's' : ''}`
: 'Added (offline — will retry on next load)';
msg.className = d.online ? 'admin-success' : 'admin-error';
@@ -728,11 +866,27 @@ function initEndpointForm() {
});
}
const ollamaBtn = el('adm-epOllamaBtn');
if (ollamaBtn) {
ollamaBtn.addEventListener('click', async () => {
const input = el('adm-epLocalUrl');
if (input) {
input.value = await _defaultOllamaUrl();
input.focus();
}
const msg = _endpointMsg('local');
if (msg) {
msg.innerHTML = '<span style="font-size:11px;opacity:0.55;">Ollama ready to test.</span>';
msg.className = '';
}
});
}
// Discover local models button
const discoverBtn = el('adm-epDiscoverBtn');
if (discoverBtn) {
discoverBtn.addEventListener('click', async () => {
const msg = el('adm-epMsg');
const msg = _endpointMsg('local');
discoverBtn.disabled = true;
// Keep the button's icon as-is while scanning; the whirlpool +
// status text below is enough feedback. (Two spinning indicators
@@ -747,7 +901,7 @@ function initEndpointForm() {
wrap.style.cssText = 'display:flex;align-items:center;padding:8px 0;';
wrap.appendChild(wp.element);
const txt = document.createElement('span');
txt.textContent = 'Scanning ports 8000-8020 for model servers...';
txt.textContent = 'Scanning ports 8000-8020 and 11434 for model servers...';
txt.style.cssText = 'font-size:12px;opacity:0.7;';
wrap.appendChild(txt);
msg.appendChild(wrap);
@@ -758,7 +912,7 @@ function initEndpointForm() {
const data = await res.json();
const items = data.items || [];
if (!items.length) {
msg.textContent = 'No model servers found. Make sure vLLM, Ollama, or similar is running.';
msg.textContent = 'No model servers found. Make sure vLLM, llama.cpp, SGLang, or Ollama is running. Docker users may need OLLAMA_HOST=0.0.0.0:11434.';
msg.className = 'admin-error';
} else {
// Auto-add each discovered endpoint
@@ -767,7 +921,7 @@ function initEndpointForm() {
const base = item.url.replace('/chat/completions', '').replace(/\/$/, '');
const fd = new FormData();
fd.append('base_url', base);
fd.append('skip_probe', 'true');
fd.append('skip_probe', 'false');
const r = await fetch('/api/model-endpoints', { method: 'POST', body: fd });
if (r.ok) {
added++;
@@ -813,6 +967,27 @@ function initEndpointForm() {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
});
});
document.querySelectorAll('.adm-quickstart-section').forEach((sec) => {
const head = sec.querySelector('.adm-quickstart-toggle');
if (!head) return;
const key = 'odysseus.addModels.' + sec.id + '.open';
let open = false;
try { open = localStorage.getItem(key) === '1'; } catch {}
const apply = () => {
sec.classList.toggle('collapsed', !open);
head.setAttribute('aria-expanded', open ? 'true' : 'false');
};
apply();
const toggle = () => {
open = !open;
try { localStorage.setItem(key, open ? '1' : '0'); } catch {}
apply();
};
head.addEventListener('click', toggle);
head.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
});
});
}
/* ═══════════════════════════════════════════