diff --git a/static/js/cookbook-deps-recipes.js b/static/js/cookbook-deps-recipes.js new file mode 100644 index 000000000..86d4f6f07 --- /dev/null +++ b/static/js/cookbook-deps-recipes.js @@ -0,0 +1,85 @@ +// Per-backend × per-model install recipes for the Dependencies tab. +// +// Each entry says: when you're about to serve `model` on `backend`, here's +// the exact shell sequence to make the venv + install the right packages. +// Entries are matched first-hit; put the more specific patterns ABOVE the +// generic fallback for that backend. + +const _RECIPES = [ + // ── vllm ────────────────────────────────────────────────────────────── + // MiniMax M2/M2.7 — same generic vllm install for now; kept as its own + // entry so future model-specific patches (FP8 quants, custom kernels) + // land in one obvious place without touching the catch-all. + { + backend: 'vllm', + label: 'MiniMax M2 / M2.7', + match: (m) => /minimax[-_]?m\s?2(\.7)?/i.test(m || ''), + commands: [ + 'uv venv', + 'source .venv/bin/activate', + 'uv pip install -U vllm --torch-backend auto', + ], + }, + // Generic vllm fallback — auto-resolves the right torch backend (CUDA + // 12.x / 12.4 / ROCm) at install time so users don't have to know. + { + backend: 'vllm', + label: 'Any vLLM model', + match: () => true, + commands: [ + 'uv venv', + 'source .venv/bin/activate', + 'uv pip install -U vllm --torch-backend auto', + ], + }, + + // ── sglang ──────────────────────────────────────────────────────────── + { + backend: 'sglang', + label: 'Any SGLang model', + match: () => true, + commands: [ + 'uv venv', + 'source .venv/bin/activate', + 'uv pip install -U "sglang[all]" --torch-backend auto', + ], + }, + + // ── llama.cpp ───────────────────────────────────────────────────────── + // The cookbook-side rebuild path covers this for users who already have + // the engine compiled — but for a fresh box, surface a sane install. + { + backend: 'llama_cpp', + label: 'Any GGUF model', + match: () => true, + commands: [ + 'uv venv', + 'source .venv/bin/activate', + 'CMAKE_ARGS="-DGGML_CUDA=on" uv pip install -U "llama-cpp-python[server]"', + ], + }, +]; + +// Backends we surface a recipe panel for. Other rows in the Dependencies +// list keep the existing flat Install/Reinstall button without an expand +// affordance. +export const RECIPE_BACKENDS = new Set(['vllm', 'sglang', 'llama_cpp']); + +// All recipe entries for a given backend, in catalog order. The first one +// is the model-specific match (when present); the last is always the +// generic fallback. +export function recipesForBackend(backend) { + return _RECIPES.filter((r) => r.backend === backend); +} + +// Pick the best recipe for a backend + model id. Returns the catalog +// fallback when nothing more specific matches, or null if the backend +// isn't in the catalog at all. +export function pickRecipe(backend, modelId) { + const candidates = recipesForBackend(backend); + if (!candidates.length) return null; + for (const r of candidates) { + try { if (r.match(modelId)) return r; } catch (_) {} + } + return candidates[candidates.length - 1] || null; +} diff --git a/static/js/cookbook.js b/static/js/cookbook.js index b825ce415..81fd48b8e 100644 --- a/static/js/cookbook.js +++ b/static/js/cookbook.js @@ -8,6 +8,7 @@ import spinnerModule from './spinner.js'; import { providerLogo } from './providers.js'; import { makeWindowDraggable } from './windowDrag.js'; import { _diagnose, _showDiagnosis, _clearDiagnosis, _runQuickCmd, ERROR_PATTERNS } from './cookbook-diagnosis.js'; +import { RECIPE_BACKENDS, recipesForBackend, pickRecipe } from './cookbook-deps-recipes.js'; import { _hwfitCache, _hwfitDebounce, _hwfitFetch, _hwfitInit, _hwfitRenderList, _hwfitRenderHw, _renderGpuToggles, _expandModelRow, _fitColors, _hwfitColumns, _cachedModelIds, _gpuToggleTotal, _resetGpuToggleState } from './cookbook-hwfit.js'; // Sub-modules @@ -811,6 +812,13 @@ async function _fetchDependencies() { } else if (pkg.name === 'sglang' && pkg.installed) { _rebuildBtn = ``; } + // For backends with a recipe catalog (vllm / sglang / llama_cpp), + // append a caret button that toggles a per-row recipe panel below. + const hasRecipe = RECIPE_BACKENDS.has(pkg.name); + const recipeCaret = hasRecipe + ? `` + : ''; + const recipePanel = hasRecipe ? _recipePanelHtml(pkg.name) : ''; return `
.
+ list.querySelectorAll('[data-dep-recipe-pick]').forEach(sel => {
+ sel.addEventListener('change', () => {
+ const backend = sel.dataset.depRecipePick;
+ const candidates = recipesForBackend(backend);
+ const recipe = candidates[parseInt(sel.value, 10) || 0];
+ if (!recipe) return;
+ const pre = list.querySelector(`[data-dep-recipe-cmds="${CSS.escape(backend)}"]`);
+ if (pre) pre.textContent = recipe.commands.join('\n');
+ });
+ });
+ // Copy: drop the visible command block on the clipboard.
+ list.querySelectorAll('[data-dep-recipe-copy]').forEach(btn => {
+ btn.addEventListener('click', async (e) => {
+ e.stopPropagation();
+ const backend = btn.dataset.depRecipeCopy;
+ const pre = list.querySelector(`[data-dep-recipe-cmds="${CSS.escape(backend)}"]`);
+ if (!pre) return;
+ try {
+ await navigator.clipboard.writeText(pre.textContent);
+ uiModule.showToast('Copied');
+ } catch {
+ // Fallback for non-secure contexts: select the pre's text so
+ // the user can Ctrl+C themselves.
+ const sel = window.getSelection(); const range = document.createRange();
+ range.selectNodeContents(pre); sel.removeAllRanges(); sel.addRange(range);
+ }
+ });
+ });
+ // Run: launch the joined `cmd1 && cmd2 && …` as a tmux task on the
+ // currently-selected deps server, same plumbing as Install.
+ list.querySelectorAll('[data-dep-recipe-run]').forEach(btn => {
+ btn.addEventListener('click', async (e) => {
+ e.stopPropagation();
+ const backend = btn.dataset.depRecipeRun;
+ const pre = list.querySelector(`[data-dep-recipe-cmds="${CSS.escape(backend)}"]`);
+ if (!pre) return;
+ const cmd = pre.textContent.split('\n').map(s => s.trim()).filter(Boolean).join(' && ');
+ const depsSel = document.getElementById('hwfit-deps-server');
+ if (depsSel) _applyServerSelection(depsSel.value);
+ const targetHost = _envState.remoteHost || 'local';
+ const reqBody = {
+ repo_id: `${backend} setup`,
+ cmd: cmd,
+ remote_host: _envState.remoteHost || undefined,
+ ssh_port: _getPort(_envState.remoteHost) || undefined,
+ platform: _envState.platform || undefined,
+ };
+ try {
+ const res = await fetch('/api/model/serve', {
+ method: 'POST', credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(reqBody),
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || !data.ok) {
+ uiModule.showToast('Run failed: ' + String(data.detail || data.error || `HTTP ${res.status}`).slice(0, 200));
+ return;
+ }
+ const payload = { repo_id: `${backend} setup`, _cmd: cmd, remote_host: _envState.remoteHost || '', _dep: true };
+ _addTask(data.session_id, `${backend} setup`, 'download', payload);
+ uiModule.showToast(`Running ${backend} setup on ${targetHost}…`);
+ } catch (err) {
+ uiModule.showToast('Run failed: ' + err.message);
+ }
+ });
+ });
+
// Wire the ⋮ menu on installed packages — currently just "Update".
function _showDepMenu(anchor) {