Merge verified Odysseus fixes

This commit is contained in:
pewdiepie-archdaemon
2026-07-23 14:49:02 +00:00
parent 4c9a8ca115
commit d8a2059df8
117 changed files with 15693 additions and 3191 deletions
+50 -29
View File
@@ -466,21 +466,22 @@ async function _selectAddedModelInChat(endpoint) {
async function loadEndpoints() {
const listLocal = el('adm-epList-local');
const listApi = el('adm-epList-api');
// Fallback to the legacy single list if the split containers don't exist
// (older HTML or third-party embedding).
const listLegacy = el('adm-epList');
// Refresh model picker so new endpoints show up in chat
if (window.modelsModule && window.modelsModule.refreshModels) {
window.modelsModule.refreshModels(true);
// Render endpoint rows first. Do not make Added Models wait on /api/models or
// endpoint probes; explicit Refresh/Probe actions do that work.
const refreshDependentModelUi = (force = false) => {
setTimeout(() => {
if (window.sessionModule && window.sessionModule.updateModelPicker) {
window.sessionModule.updateModelPicker();
if (window.modelsModule && window.modelsModule.refreshModels) {
window.modelsModule.refreshModels(!!force, force ? {} : { cacheOnly: true }).then(() => {
if (window.sessionModule && window.sessionModule.updateModelPicker) {
window.sessionModule.updateModelPicker();
}
}).catch(() => {});
}
}, 1500);
}
if (settingsModule && typeof settingsModule.refreshAiModelEndpoints === 'function') {
settingsModule.refreshAiModelEndpoints();
}
if (settingsModule && typeof settingsModule.refreshAiModelEndpoints === 'function') {
settingsModule.refreshAiModelEndpoints();
}
}, 0);
};
try {
const res = await fetch('/api/model-endpoints', { credentials: 'same-origin' });
// Treat a non-OK response (e.g. 401/403 for non-admins, or backend
@@ -495,13 +496,16 @@ async function loadEndpoints() {
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;
refreshDependentModelUi();
return;
}
const rowHtml = data.map(ep => {
const epModels = Array.isArray(ep.models) ? ep.models : [];
const visibleCount = epModels.length;
const totalCount = visibleCount + (ep.hidden_count || 0);
const pinnedModels = Array.isArray(ep.pinned_models) ? ep.pinned_models : [];
const visibleCount = ep.picker_requires_pinning ? pinnedModels.length : epModels.length;
const totalCount = Number.isFinite(Number(ep.model_count))
? Number(ep.model_count)
: visibleCount + (ep.hidden_count || 0);
// `ep.models` is the *visible* set — when every model is hidden it's
// empty, but we still need to render the expand panel so the user can
// un-hide them. Gate on the total instead.
@@ -562,17 +566,21 @@ async function loadEndpoints() {
apiIdx.sort(_sortByEnabled);
_renderInto(listLocal, localIdx);
_renderInto(listApi, apiIdx);
if (listLegacy) listLegacy.innerHTML = rowHtml.join('');
// Iterate matching nodes across both containers.
const queryAll = (sel) => {
const out = [];
[listLocal, listApi, listLegacy].forEach(c => {
[listLocal, listApi].forEach(c => {
if (c) c.querySelectorAll(sel).forEach(n => out.push(n));
});
return out;
};
queryAll('[data-adm-toggle-ep]').forEach(btn => {
btn.addEventListener('click', async (e) => { e.stopPropagation(); await fetch(`/api/model-endpoints/${btn.dataset.admToggleEp}`, { method: 'PATCH' }); loadEndpoints(); });
btn.addEventListener('click', async (e) => {
e.stopPropagation();
await fetch(`/api/model-endpoints/${btn.dataset.admToggleEp}`, { method: 'PATCH' });
await _refreshAfterEndpointChange();
loadEndpoints();
});
});
queryAll('[data-adm-copy-url]').forEach(btn => {
btn.addEventListener('click', (e) => {
@@ -663,6 +671,8 @@ async function loadEndpoints() {
const _loadingHtml = (label) => `<span style="opacity:0.55;font-size:11px;display:inline-flex;align-items:center;gap:8px;">${esc(label)}</span>`;
const renderModels = (models, warning = '') => {
const sortedModels = sortModelObjects(models);
const usesPinnedPicker = sortedModels.some(m => !!m.picker_requires_pinning);
panel.dataset.pickerMode = usesPinnedPicker ? 'pinned' : 'hidden';
const warningHtml = warning ? `<div class="admin-error" style="font-size:11px;margin:6px 0;">${esc(warning)}</div>` : '';
const attachRefresh = () => {
panel.querySelector(`[data-ep-refresh-models="${epId}"]`)?.addEventListener('click', async (e) => {
@@ -674,6 +684,7 @@ async function loadEndpoints() {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const refreshedModels = await res.json();
renderModels(refreshedModels, refreshWarning);
_refreshAfterEndpointChange();
if (refreshWarning && uiModule?.showToast) uiModule.showToast(refreshWarning, 6000);
} catch (_) {
renderModels(sortedModels, 'Model refresh failed; kept cached models.');
@@ -684,26 +695,26 @@ async function loadEndpoints() {
panel.innerHTML = `<div class="mcp-tools-header">
<span>Models</span>
<span style="display:flex;gap:8px;align-items:center;">
<span class="mcp-tools-count">0/0 enabled</span>
<a href="#" data-ep-refresh-models="${epId}">Refresh</a>
</span>
</div>${warningHtml}<span style="opacity:0.5;font-size:11px;">No models</span>`;
attachRefresh();
return;
}
const hiddenSet = new Set(sortedModels.filter(m => m.is_hidden).map(m => m.id));
const enabledCount = usesPinnedPicker
? sortedModels.filter(m => m.is_pinned).length
: sortedModels.filter(m => !m.is_hidden).length;
const showSearch = sortedModels.length >= 8;
panel.innerHTML = `<div class="mcp-tools-header">
<span>Models</span>
<span style="display:flex;gap:8px;align-items:center;">
<span class="mcp-tools-count">${sortedModels.length - hiddenSet.size}/${sortedModels.length} enabled</span>
<a href="#" data-ep-refresh-models="${epId}">Refresh</a>
<a href="#" data-ep-select-all="${epId}">All</a>
<a href="#" data-ep-select-none="${epId}">None</a>
</span>
</div>${warningHtml}${showSearch ? `<input type="search" class="mcp-tools-search" placeholder="Search ${sortedModels.length} models..." data-ep-search="${epId}">` : ''}<div class="mcp-tools-list">` + sortedModels.map(m =>
`<label title="${esc(m.id)}" data-ep-model-row data-search="${esc((m.display + ' ' + m.id).toLowerCase())}" class="adm-model-row">
<input type="checkbox" class="adm-cb-hidden" data-ep-model-id="${esc(m.id)}" ${!m.is_hidden ? 'checked' : ''}>
<input type="checkbox" class="adm-cb-hidden" data-ep-model-id="${esc(m.id)}" ${(usesPinnedPicker ? m.is_pinned : !m.is_hidden) ? 'checked' : ''}>
<span class="adm-check-dot" aria-hidden="true"></span>
<span>${esc(m.display)}</span>
</label>`
@@ -744,35 +755,43 @@ async function loadEndpoints() {
}
});
});
refreshDependentModelUi();
} catch (e) {
const err = '<div class="admin-error">Failed to load</div>';
[listLocal, listApi, listLegacy].forEach(c => { if (c) c.innerHTML = err; });
[listLocal, listApi].forEach(c => { if (c) c.innerHTML = err; });
}
}
async function _saveEpModelState(epId, panel) {
const hidden = [];
const pinned = [];
const usesPinnedPicker = panel && panel.dataset && panel.dataset.pickerMode === 'pinned';
panel.querySelectorAll('input[type=checkbox]').forEach(cb => {
if (!cb.checked) hidden.push(cb.dataset.epModelId);
if (cb.checked) pinned.push(cb.dataset.epModelId);
else hidden.push(cb.dataset.epModelId);
});
const total = panel.querySelectorAll('input[type=checkbox]').length;
const enabled = usesPinnedPicker ? pinned.length : total - hidden.length;
try {
await fetch(`/api/model-endpoints/${epId}/models`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ hidden }),
body: JSON.stringify(usesPinnedPicker ? { pinned_models: pinned } : { hidden }),
});
const countLabel = panel.querySelector('.mcp-tools-count');
if (countLabel) countLabel.textContent = `${total - hidden.length}/${total} enabled`;
const row = panel.closest('[data-adm-ep-id]');
if (row) {
const badge = row.querySelector('.admin-badge');
if (badge && !badge.classList.contains('admin-badge-off')) badge.textContent = `${total - hidden.length}/${total} models enabled`;
if (badge && !badge.classList.contains('admin-badge-off')) {
const match = String(badge.textContent || '').match(/\/(\d+)/);
const canonicalTotal = match ? Number(match[1]) : total;
badge.textContent = `${enabled}/${canonicalTotal} models enabled`;
}
}
if (settingsModule && typeof settingsModule.refreshAiModelEndpoints === 'function') {
settingsModule.refreshAiModelEndpoints();
}
_refreshAfterEndpointChange();
} catch (e) { /* silent */ }
}
@@ -1480,6 +1499,7 @@ function initEndpointForm() {
})());
await Promise.all(workers);
await loadEndpoints();
await _refreshAfterEndpointChange();
_refreshOfflineCount();
if (uiModule && uiModule.showToast) {
const ok = Math.max(0, ids.length - failed);
@@ -1522,6 +1542,7 @@ function initEndpointForm() {
await Promise.all(ids.map(id =>
fetch('/api/model-endpoints/' + id, { method: 'DELETE', credentials: 'same-origin' }).catch(() => {})
));
await _refreshAfterEndpointChange();
try { await loadEndpoints(); } catch (_) {}
_refreshOfflineCount();
if (uiModule && uiModule.showToast) uiModule.showToast(`Removed ${ids.length} offline endpoint${ids.length === 1 ? '' : 's'}`, 1800);