First Run

The first time LlamaForge starts with a fresh config.json (no ui_mode key on disk), config.py's migrate() classifies the install: if server_bin is already set (an existing llama.cpp build was found), the config is marked ui_mode: "advanced" and onboarded: true; otherwise it is marked ui_mode: "lite" and onboarded: false, which is what causes the onboarding wizard to appear. This check is idempotent — a config that already carries ui_mode is left untouched on every later startup.

Fresh-install safety net

Several things that used to derail a first run are now handled automatically, in both run.ps1 / run.sh and the backend:

Lite vs Advanced mode

LlamaForge has two UI densities, toggled at any time from the Lite / Advanced switch in the sidebar (applyMode() in web/js/ui.js toggles a mode-lite class on <body>; setMode() persists the choice via POST /api/config with ui_mode):

Finishing the wizard and skipping it both set ui_mode to "lite" (skipping is "not now", not "I'm an expert"). You can switch between them afterward at any time.

The onboarding wizard

initWizard() in web/js/wizard.js shows the wizard automatically whenever the server reports onboarding.onboarded as false. It walks five steps, defined in WIZ.steps in web/js/wizard.js:

  1. Engine — "How do you want to get it?" Choose Download the official build for this PC (the default: about a minute, no compiler), I already have llama-server built, or Build from source (the Build tab; needs CMake and a compiler). The download choice shows the same engine card as the Build / Update tab, and its install keeps running in the background if you move on.
  2. Hardware — a read-only summary of detected GPUs and their VRAM (or "No GPU detected — CPU mode" if none).
  3. Model — pick an already-registered model from a dropdown. If none are registered, the step offers Get your first model: starter models sized to your GPU, one click to download and add one (with a best fit tag on the recommended one), plus a link to browse Hugging Face in Discover.
  4. Tune — choose a goal (Balanced, Max speed, Max context, or Coding) and click Auto-tune to call /api/autotune/recommend for that model and intent; the resulting knobs and their rationale are shown in a table. An optional Refine with a quick test (~1 min) button calls /api/autotune/refine to try a few high-impact variants (e.g. alternate ubatch-size/batch-size) with one short generation each and keep the fastest. It is a quick check, not llama-bench: results move with whatever else the machine is doing.
  5. Ready — confirms the chosen settings will be applied to the selected model and it will be loaded.

Finishing the wizard saves the recommended knobs with /api/save, loads the model with /api/load, and marks the config onboarded: true, ui_mode: "lite" regardless of whether the load itself succeeded (a failed load surfaces a toast telling you to load it manually from the Models tab). When the load works, the dashboard then opens the Chat tab on that model. Skip instead marks the config onboarded: true, ui_mode: "lite" and closes the wizard without touching any model.

What auto-tune decides

backend/autotune.py's recommend(meta, hw, intent, size_bytes) is a pure function that turns a GGUF's header metadata and the detected hardware into a small set of knobs — everything else is left at llama.cpp's own defaults. Four intents are supported: balanced, speed, context, coding.

Every knob recommend() sets comes with a plain-language reason, which is what populates the rationale column in the wizard's Tune step.

Auto-tune only writes the handful of knobs above. Anything else you set by hand on the Models tab is kept. If you pin ctx-size, n-gpu-layers or tensor-split yourself, llama.cpp's fit switches off for that model, and a load that runs out of memory will tell you so.