From 600fa6be8ac6d8611aad2dcacfa5e1680325b6c1 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Sun, 14 Jun 2026 22:33:49 +0900 Subject: [PATCH] Cookbook/Dependencies: per-backend recipe panel (vllm/sglang/llama_cpp) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each row for vllm, sglang, llama_cpp now carries an expand caret that opens an inline recipe panel below the row. The panel has: - 'Serving which model?' select populated from a new tiny catalog -
 code block showing the exact shell sequence for that pair
  - Copy: clipboard the commands
  - Run: launch the joined 'cmd1 && cmd2 && …' as a tmux task on the
    currently-selected deps server (same plumbing as Install)

New file: src/static/js/cookbook-deps-recipes.js — single source of
truth for the recipes. Seeded with MiniMax M2/M2.7 + a generic fallback
for each backend (all three use 'uv venv → source .venv/bin/activate
→ uv pip install ... --torch-backend auto', the recipe the user
pasted). Adding model-specific recipes is now a one-entry edit.

Next commit: Launch-tab pre-flight that intercepts the serve click
when the backend isn't installed and deep-links into this panel.
---
 static/js/cookbook-deps-recipes.js |  85 +++++++++++++++++++++
 static/js/cookbook.js              | 117 ++++++++++++++++++++++++++++-
 2 files changed, 200 insertions(+), 2 deletions(-)
 create mode 100644 static/js/cookbook-deps-recipes.js

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 `
` + `
` + `
${esc(pkg.name)}
` @@ -821,9 +829,31 @@ async function _fetchDependencies() { + _rebuildBtn + `${esc(pkg.category)}` + _statusTag(pkg, isLocal, isSystemDep, winBlocked) - + `
`; + + recipeCaret + + `
` + + recipePanel; }; + // Per-backend recipe panel (model picker + commands + Copy/Run). + // Lives directly below the row it expands and starts collapsed. + function _recipePanelHtml(backend) { + const candidates = recipesForBackend(backend); + if (!candidates.length) return ''; + const opts = candidates.map((r, i) => ``).join(''); + const initial = candidates[0]; + return ``; + } + const _section = (title, note, items) => items.length ? `
${title}${note}
` + items.map(_depRow).join('') @@ -920,7 +950,7 @@ async function _fetchDependencies() { } // Wire install buttons (not-installed packages) - list.querySelectorAll('.cookbook-dep-install').forEach(btn => { + list.querySelectorAll('.cookbook-dep-install:not(.cookbook-dep-recipe-run)').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const pipName = btn.dataset.depPip; @@ -929,6 +959,89 @@ async function _fetchDependencies() { }); }); + // ── Recipe panel wiring (per-backend dropdown with model + commands) ── + // Caret toggle: shows/hides the panel directly below the backend row. + list.querySelectorAll('[data-dep-recipe-toggle]').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const backend = btn.dataset.depRecipeToggle; + const panel = list.querySelector(`[data-dep-recipe-panel="${CSS.escape(backend)}"]`); + if (!panel) return; + const open = panel.style.display === 'none' || !panel.style.display; + panel.style.display = open ? 'block' : 'none'; + btn.setAttribute('aria-expanded', open ? 'true' : 'false'); + const caret = btn.querySelector('svg'); + if (caret) caret.style.transform = open ? 'rotate(180deg)' : ''; + }); + }); + // Model select: re-pick the matching recipe and refresh the
.
+    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) {