mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-10 07:28:41 -04:00
Merge verified Odysseus fixes
This commit is contained in:
+190
-32
@@ -40,7 +40,13 @@ import { openCookbookDependencies } from './cookbook-diagnosis.js';
|
||||
// Map a serve-backend code (vllm / sglang / llamacpp / mlx) → the package name
|
||||
// the Dependencies API reports. Used to look up "is this backend installed
|
||||
// on the target server" before firing a launch.
|
||||
const _BACKEND_PKG = { vllm: 'vllm', sglang: 'sglang', llamacpp: 'llama_cpp', mlx: 'mlx_lm' };
|
||||
const _BACKEND_PKG = { vllm: 'vllm', sglang: 'sglang', llamacpp: 'llama_cpp', mlx: 'mlx_lm', mlx_image: 'mflux', diffusers: 'diffusers' };
|
||||
function _dependencyPkgForModel(runBackend, modelName = '') {
|
||||
const nm = String(modelName || '').toLowerCase();
|
||||
if (runBackend === 'mlx_image' && nm.includes('boogu')) return 'boogu_image_mlx';
|
||||
if (runBackend === 'diffusers' && nm.includes('krea')) return 'krea_diffusers';
|
||||
return _BACKEND_PKG[runBackend];
|
||||
}
|
||||
|
||||
function _normalizeCookbookModelDir(dir) {
|
||||
const d = String(dir || '').replaceAll('\u2715', '').replaceAll('\u2716', '').trim();
|
||||
@@ -95,7 +101,7 @@ function _wireServerColorPicker(entry) {
|
||||
// the target server. Returns true if it's good to go, false if we should
|
||||
// block and route the user into Dependencies.
|
||||
async function _ensureBackendInstalled(runBackend, host, port, envPath, modelName) {
|
||||
const pkgName = _BACKEND_PKG[runBackend];
|
||||
const pkgName = _dependencyPkgForModel(runBackend, modelName);
|
||||
if (!pkgName) return true; // unknown backend — don't block
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
@@ -542,9 +548,31 @@ function _hwfitShowError(list, host, detail) {
|
||||
// needed. Ollama rows are merged into the main list (see _ensureOllamaLib +
|
||||
// _ollamaToHwfitRows below) so the filter handles all engines uniformly.
|
||||
function _applyEngineFilter(models) {
|
||||
let out = Array.isArray(models) ? models : [];
|
||||
const useCase = document.getElementById('hwfit-usecase')?.value || '';
|
||||
const srv = _serverByVal(_envState.remoteServerKey || _envState.remoteHost);
|
||||
const platform = String(srv?.platform || _envState.platform || _hwfitCache?.system?.platform || '').toLowerCase();
|
||||
const backend = String(_hwfitCache?.system?.backend || '').toLowerCase();
|
||||
const gpuName = String(_hwfitCache?.system?.gpu_name || '').toLowerCase();
|
||||
const isAppleTarget = !!(useCase === 'image_gen' && (
|
||||
platform === 'darwin'
|
||||
|| platform === 'macos'
|
||||
|| platform.includes('mac')
|
||||
|| backend === 'metal'
|
||||
|| backend === 'mps'
|
||||
|| backend === 'apple'
|
||||
|| gpuName.includes('apple')
|
||||
|| _hwfitCache?.system?.unified_memory
|
||||
));
|
||||
if (isAppleTarget) {
|
||||
out = out.filter(m => {
|
||||
const text = `${m?.name || ''} ${m?.id || ''} ${m?.provider || ''}`.toLowerCase();
|
||||
return text.includes('mlx-community/') || text.includes('mlx-community') || m?.mlx_only || m?.apple_ok;
|
||||
});
|
||||
}
|
||||
const want = document.getElementById('hwfit-engine')?.value || '';
|
||||
if (!want || !Array.isArray(models)) return models || [];
|
||||
return models.filter(m => {
|
||||
if (!want) return out;
|
||||
return out.filter(m => {
|
||||
try { return _detectBackend(m).backend === want; } catch { return true; }
|
||||
});
|
||||
}
|
||||
@@ -794,7 +822,7 @@ export async function _hwfitFetch(fresh = false, opts = {}) {
|
||||
if (v !== '') params.set(k, v);
|
||||
});
|
||||
if (hasManualOrDismissed) params.set('_hw_override_ts', String(Date.now()));
|
||||
// Image models use a separate registry/endpoint
|
||||
// Image models use a separate registry/endpoint.
|
||||
const isImageMode = useCase === 'image_gen';
|
||||
if ((fresh || (_paintedFromCache && !search)) && !isImageMode) {
|
||||
params.set('refresh_catalog', '1'); // update HF-backed dynamic catalogs in the background
|
||||
@@ -840,7 +868,7 @@ export async function _hwfitFetch(fresh = false, opts = {}) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Normalize image model fields to match LLM renderer expectations
|
||||
// Normalize image model fields to match LLM renderer expectations.
|
||||
if (isImageMode && data.models) {
|
||||
data.models = data.models.map(m => ({
|
||||
...m,
|
||||
@@ -1263,9 +1291,46 @@ export const _hwfitColumns = [
|
||||
{ key: null, label: 'Mode', cls: 'hwfit-c-mode' },
|
||||
];
|
||||
|
||||
function _sortHwfitRows(models) {
|
||||
const rows = Array.isArray(models) ? [...models] : [];
|
||||
const sortSel = document.getElementById('hwfit-sort');
|
||||
const sortKey = sortSel?.value || 'newest';
|
||||
const asc = sortSel?.dataset.reverse === '1';
|
||||
if (sortKey === 'fit') {
|
||||
const fitRank = { perfect: 4, good: 3, marginal: 2, too_tight: 1, no_fit: 0 };
|
||||
rows.sort((a, b) => {
|
||||
const ar = fitRank[a.fit_level] ?? -1;
|
||||
const br = fitRank[b.fit_level] ?? -1;
|
||||
if (ar !== br) return asc ? ar - br : br - ar;
|
||||
const as = Number(a.score) || 0;
|
||||
const bs = Number(b.score) || 0;
|
||||
return asc ? as - bs : bs - as;
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
if (sortKey === 'newest') {
|
||||
rows.sort((a, b) => {
|
||||
const ad = String(a.release_date || '');
|
||||
const bd = String(b.release_date || '');
|
||||
if (ad === bd) return 0;
|
||||
if (!ad) return 1;
|
||||
if (!bd) return -1;
|
||||
return asc ? (ad < bd ? -1 : 1) : (ad < bd ? 1 : -1);
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
const field = { score: 'score', vram: 'required_gb', speed: 'speed_tps', params: 'params_b', context: 'context' }[sortKey] || 'score';
|
||||
rows.sort((a, b) => {
|
||||
const av = Number(a[field]) || 0;
|
||||
const bv = Number(b[field]) || 0;
|
||||
return asc ? av - bv : bv - av;
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function _hwfitRenderList(el, models) {
|
||||
if (!el) return;
|
||||
models = models || [];
|
||||
models = _sortHwfitRows(models);
|
||||
if (!models.length) {
|
||||
// Disambiguate WHY the list is empty so capable servers don't read as "too weak":
|
||||
// active filters vs. a likely under-reported probe vs. genuinely low hardware.
|
||||
@@ -1570,7 +1635,9 @@ export function _expandModelRow(row, modelData) {
|
||||
html += `</div>`;
|
||||
html += `<div class="hwfit-panel-actions">`;
|
||||
html += `<button class="cookbook-btn hwfit-dl-btn">Download</button>`;
|
||||
if (!modelData.is_image_gen) {
|
||||
if (modelData.is_image_gen) {
|
||||
html += `<button class="cookbook-btn cookbook-run-btn hwfit-quickrun-btn" title="Download + run as an image endpoint">Run Image</button>`;
|
||||
} else {
|
||||
html += `<button class="cookbook-btn cookbook-run-btn hwfit-quickrun-btn" title="Download + launch with smart defaults">Run</button>`;
|
||||
html += `<button class="cookbook-btn hwfit-serve-expand-btn" title="Configure & serve">Configure</button>`;
|
||||
}
|
||||
@@ -1653,30 +1720,34 @@ export function _expandModelRow(row, modelData) {
|
||||
const _clashing = _allServes.filter(t => _taskPort(t) === _qrPort);
|
||||
if (_clashing.length) {
|
||||
const _names = _clashing.map(t => t.payload?.repo_id || t.repo || t.name || '?').filter(Boolean);
|
||||
const _ok = await window.styledConfirm?.(
|
||||
`${_clashing.length} model${_clashing.length === 1 ? '' : 's'} on port ${_qrPort} (${_names.join(', ')}). Stop it and launch this one?`,
|
||||
{ confirmText: 'Stop & launch', cancelText: 'Cancel' }
|
||||
const _choice = await window.styledConfirm?.(
|
||||
`${_clashing.length} model${_clashing.length === 1 ? '' : 's'} on port ${_qrPort} (${_names.join(', ')}). Stop it first, or launch anyway?`,
|
||||
{ title: `Port ${_qrPort} in use`, confirmText: 'Stop & launch', alternateText: 'Launch anyway', cancelText: 'Cancel' }
|
||||
);
|
||||
if (!_ok) return;
|
||||
quickRunBtn.disabled = true;
|
||||
quickRunBtn.textContent = 'Stopping…';
|
||||
for (const t of _clashing) {
|
||||
try {
|
||||
const _taskEl = document.querySelector(`.cookbook-task[data-task-id="${t.sessionId}"]`);
|
||||
const _stopBtn = _taskEl?.querySelector('.cookbook-task-action-stop');
|
||||
if (_stopBtn) {
|
||||
_stopBtn.click();
|
||||
} else {
|
||||
await fetch('/api/shell/exec', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command: _tmuxGracefulKill(t) }),
|
||||
});
|
||||
if (!_choice) return;
|
||||
if (_choice === 'alternate') {
|
||||
uiModule.showToast('Launching anyway. If the port is already occupied, the new serve may fail.', 6000);
|
||||
} else {
|
||||
quickRunBtn.disabled = true;
|
||||
quickRunBtn.textContent = 'Stopping…';
|
||||
for (const t of _clashing) {
|
||||
try {
|
||||
const _taskEl = document.querySelector(`.cookbook-task[data-task-id="${t.sessionId}"]`);
|
||||
const _stopBtn = _taskEl?.querySelector('.cookbook-task-action-stop');
|
||||
if (_stopBtn) {
|
||||
_stopBtn.click();
|
||||
} else {
|
||||
await fetch('/api/shell/exec', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command: _tmuxGracefulKill(t) }),
|
||||
});
|
||||
}
|
||||
} catch (_killErr) { /* best-effort */ }
|
||||
}
|
||||
} catch (_killErr) { /* best-effort */ }
|
||||
await new Promise(r => setTimeout(r, 2500));
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 2500));
|
||||
}
|
||||
} catch (_e) { /* best-effort */ }
|
||||
|
||||
@@ -1808,9 +1879,12 @@ export function _expandModelRow(row, modelData) {
|
||||
cmd += ` --context-length ${maxCtx}`;
|
||||
cmd += ` --mem-fraction-static ${gpuUtil}`;
|
||||
cmd += ' --trust-remote-code';
|
||||
} else if (runBackend === 'mlx_image') {
|
||||
const bindHost = host ? '0.0.0.0' : '127.0.0.1';
|
||||
cmd = `python3 scripts/mlx_image_server.py --model ${_shellQuote(modelData.name)} --host ${bindHost} --port ${port} --steps 20`;
|
||||
} else if (runBackend === 'mlx') {
|
||||
const bindHost = host ? '0.0.0.0' : '127.0.0.1';
|
||||
cmd = `python3 -m mlx_lm.server --model ${_shellQuote(modelData.name)} --host ${bindHost} --port ${port}`;
|
||||
cmd = `python3 -m mlx_lm.server --model ${_shellQuote(modelData.name)} --host ${bindHost} --port ${port} --max-tokens ${maxCtx}`;
|
||||
} else if (runBackend === 'llamacpp') {
|
||||
const dir = `"$HOME/.cache/huggingface/hub/models--${modelData.name.replace(/\//g, '--')}/snapshots"`;
|
||||
const ggufPath = `$({ find ${dir} -name '*-00001-of-*.gguf' 2>/dev/null | sort; find ${dir} -name '*.gguf' 2>/dev/null | sort; } | head -1)`;
|
||||
@@ -1852,7 +1926,7 @@ export function _expandModelRow(row, modelData) {
|
||||
);
|
||||
if (!_ok) {
|
||||
quickRunBtn.disabled = false;
|
||||
quickRunBtn.textContent = 'Run';
|
||||
quickRunBtn.textContent = modelData.is_image_gen ? 'Run Image' : 'Run';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1889,7 +1963,7 @@ export function _expandModelRow(row, modelData) {
|
||||
uiModule.showError('Launch failed: ' + e.message);
|
||||
}
|
||||
quickRunBtn.disabled = false;
|
||||
quickRunBtn.textContent = 'Run';
|
||||
quickRunBtn.textContent = modelData.is_image_gen ? 'Run Image' : 'Run';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1938,6 +2012,89 @@ function _hwfitEngineGlyph(value) {
|
||||
return _HWFIT_ENGINE_GLYPHS[value] || _HWFIT_ENGINE_GLYPHS[''];
|
||||
}
|
||||
|
||||
const _HWFIT_USECASE_GLYPHS = {
|
||||
general: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>',
|
||||
multimodal: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>',
|
||||
image_gen: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><path d="M21 15l-5-5L5 21"></path></svg>',
|
||||
};
|
||||
|
||||
function _hwfitUsecaseGlyph(value) {
|
||||
return _HWFIT_USECASE_GLYPHS[value] || _HWFIT_USECASE_GLYPHS.general;
|
||||
}
|
||||
|
||||
function _bindHwfitUsecasePicker(usecase) {
|
||||
const wrap = usecase?.closest('.hwfit-usecase-wrap');
|
||||
const btn = wrap?.querySelector('[data-hwfit-usecase-btn]');
|
||||
const menu = wrap?.querySelector('[data-hwfit-usecase-menu]');
|
||||
const icon = wrap?.querySelector('[data-hwfit-usecase-icon]');
|
||||
const label = wrap?.querySelector('[data-hwfit-usecase-label]');
|
||||
if (!usecase || !wrap || !btn || !menu) return;
|
||||
usecase.querySelectorAll('option[value="video_gen"]').forEach((opt) => opt.remove());
|
||||
menu.querySelectorAll('[data-hwfit-usecase-value="video_gen"], .hwfit-usecase-item').forEach((item) => {
|
||||
if (item.dataset.hwfitUsecaseValue === 'video_gen' || item.textContent?.trim() === 'Video') item.remove();
|
||||
});
|
||||
if (usecase.value === 'video_gen') {
|
||||
usecase.value = 'general';
|
||||
usecase.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
if (wrap.dataset.usecasePickerBound) return;
|
||||
wrap.dataset.usecasePickerBound = '1';
|
||||
|
||||
const setOpen = (open) => {
|
||||
menu.hidden = !open;
|
||||
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
};
|
||||
const currentLabel = () => {
|
||||
const opt = Array.from(usecase.options).find((o) => o.value === usecase.value);
|
||||
return opt?.textContent || 'Standard';
|
||||
};
|
||||
const syncButton = () => {
|
||||
if (label) label.textContent = currentLabel();
|
||||
if (icon) icon.innerHTML = _hwfitUsecaseGlyph(usecase.value);
|
||||
menu.querySelectorAll('[data-hwfit-usecase-value]').forEach((item) => {
|
||||
const active = item.dataset.hwfitUsecaseValue === usecase.value;
|
||||
item.classList.toggle('active', active);
|
||||
item.setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
});
|
||||
};
|
||||
const renderMenu = () => {
|
||||
menu.innerHTML = Array.from(usecase.options).filter((opt) => opt.value !== 'video_gen').map((opt) => (
|
||||
`<button type="button" role="option" class="hwfit-usecase-item" data-hwfit-usecase-value="${opt.value}">`
|
||||
+ `<span class="hwfit-usecase-item-icon" aria-hidden="true">${_hwfitUsecaseGlyph(opt.value)}</span>`
|
||||
+ `<span class="hwfit-usecase-item-label">${opt.textContent}</span>`
|
||||
+ '</button>'
|
||||
)).join('');
|
||||
menu.querySelectorAll('[data-hwfit-usecase-value]').forEach((item) => {
|
||||
item.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const next = item.dataset.hwfitUsecaseValue || 'general';
|
||||
if (usecase.value !== next) {
|
||||
usecase.value = next;
|
||||
usecase.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
syncButton();
|
||||
setOpen(false);
|
||||
});
|
||||
});
|
||||
syncButton();
|
||||
};
|
||||
|
||||
btn.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
setOpen(menu.hidden);
|
||||
});
|
||||
usecase.addEventListener('change', syncButton);
|
||||
document.addEventListener('click', (ev) => {
|
||||
if (!wrap.contains(ev.target)) setOpen(false);
|
||||
});
|
||||
document.addEventListener('keydown', (ev) => {
|
||||
if (ev.key === 'Escape') setOpen(false);
|
||||
});
|
||||
renderMenu();
|
||||
}
|
||||
|
||||
function _bindHwfitEnginePicker(engine) {
|
||||
const wrap = engine?.closest('.hwfit-engine-wrap');
|
||||
const btn = wrap?.querySelector('[data-hwfit-engine-btn]');
|
||||
@@ -2011,6 +2168,7 @@ export function _hwfitInit() {
|
||||
const search = document.getElementById('hwfit-search');
|
||||
const remote = document.getElementById('hwfit-host');
|
||||
_syncCtxControl();
|
||||
if (uc) _bindHwfitUsecasePicker(uc);
|
||||
if (uc) uc.addEventListener('change', () => _hwfitFetch());
|
||||
if (sort) sort.addEventListener('change', () => _hwfitFetch());
|
||||
if (qpref) qpref.addEventListener('change', () => _hwfitFetch());
|
||||
|
||||
Reference in New Issue
Block a user