Merge branch 'main' into pip

This commit is contained in:
Daniel Han 2026-03-19 02:31:50 -07:00
commit 9d1e3c38bc
21 changed files with 638 additions and 76 deletions

View file

@ -19,7 +19,7 @@ Run and train AI models with a unified local interface.
<a href="https://unsloth.ai/docs/new/studio">
<img alt="unsloth studio ui homepage" src="https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/studio%20github%20landscape%20colab%20display.png" style="max-width: 100%; margin-bottom: 0;"></a>
Unsloth Studio lets you run and train text, [audio](https://unsloth.ai/docs/basics/text-to-speech-tts-fine-tuning), [embedding](https://unsloth.ai/docs/new/embedding-finetuning), [vision](https://unsloth.ai/docs/basics/vision-fine-tuning) models on Windows, Linux and macOS.
Unsloth Studio (Beta) lets you run and train text, [audio](https://unsloth.ai/docs/basics/text-to-speech-tts-fine-tuning), [embedding](https://unsloth.ai/docs/new/embedding-finetuning), [vision](https://unsloth.ai/docs/basics/vision-fine-tuning) models on Windows, Linux and macOS.
## ⭐ Features
Unsloth provides several key features for both inference and training:
@ -42,7 +42,7 @@ Unsloth provides several key features for both inference and training:
Unsloth can be used in two ways: through **[Unsloth Studio](https://unsloth.ai/docs/new/studio/)**, the web UI, or through **Unsloth Core**, the code-based version. Each has different requirements.
### Unsloth Studio (web UI)
Unsloth Studio works on **Windows, Linux, WSL** and **macOS**.
Unsloth Studio (Beta) works on **Windows, Linux, WSL** and **macOS**.
* **CPU:** Supported for **chat inference only**
* **NVIDIA:** Training works on RTX 30/40/50, Blackwell, DGX Spark, Station and more
@ -51,7 +51,26 @@ Unsloth Studio works on **Windows, Linux, WSL** and **macOS**.
* **Coming soon:** Training support for Apple MLX, AMD, and Intel.
* **Multi-GPU:** Available now, with a major upgrade on the way
#### MacOS, Linux or WSL Setup (One time):
#### MacOS, Linux, WSL Setup:
```bash
curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/install.sh | sh
```
If you don't have `curl`, use `wget`. Then to launch after setup:
```bash
source unsloth_studio/bin/activate
unsloth studio -H 0.0.0.0 -p 8888
```
#### Windows PowerShell Setup:
```powershell
irm https://raw.githubusercontent.com/unslothai/unsloth/main/install.ps1 | iex
```
Then to launch after setup:
```powershell
& .\unsloth_studio\Scripts\unsloth.exe studio -H 0.0.0.0 -p 8888
```
#### MacOS, Linux, WSL developer installs:
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv unsloth_studio --python 3.13
@ -60,14 +79,9 @@ uv pip install unsloth --torch-backend=auto
unsloth studio setup
unsloth studio -H 0.0.0.0 -p 8888
```
Then to launch every time:
```bash
source unsloth_studio/bin/activate
unsloth studio -H 0.0.0.0 -p 8888
```
#### Windows PowerShell (One time):
```bash
#### Windows PowerShell developer installs:
```powershell
winget install -e --id Python.Python.3.13
winget install --id=astral-sh.uv -e
uv venv unsloth_studio --python 3.13
@ -76,15 +90,18 @@ uv pip install unsloth --torch-backend=auto
unsloth studio setup
unsloth studio -H 0.0.0.0 -p 8888
```
Then to launch every time:
#### Docker
Use our [Docker image](https://hub.docker.com/r/unsloth/unsloth) ```unsloth/unsloth``` container. Run:
```bash
.\unsloth_studio\Scripts\activate
unsloth studio -H 0.0.0.0 -p 8888
```
docker run -d -e JUPYTER_PASSWORD="mypassword" \
-p 8888:8888 -p 8000:8000 -p 2222:22 \
-v $(pwd)/work:/workspace/work \
--gpus all \
unsloth/unsloth
```
Use our [Docker image](https://hub.docker.com/r/unsloth/unsloth) ```unsloth/unsloth``` container. Read our [Docker Guide](https://unsloth.ai/docs/get-started/install/docker).
#### Nightly Installation - MacOS, Linux or WSL Setup (One time):
#### Nightly Install - MacOS, Linux, WSL:
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
git clone --filter=blob:none https://github.com/unslothai/unsloth.git unsloth_studio
@ -102,7 +119,8 @@ source .venv/bin/activate
unsloth studio -H 0.0.0.0 -p 8888
```
#### Nightly Installation - Windows Powershell (One time):
#### Nightly Install - Windows:
Run in Windows Powershell:
```bash
winget install -e --id Python.Python.3.13
winget install --id=astral-sh.uv -e

View file

@ -4,8 +4,52 @@ set -euo pipefail
# 1. Build frontend (Vite outputs to dist/)
cd studio/frontend
# Clean stale dist to force a full rebuild
rm -rf dist
# Tailwind v4's oxide scanner respects .gitignore in parent directories.
# Python venvs create a .gitignore with "*" (ignore everything), which
# prevents Tailwind from scanning .tsx source files for class names.
# Temporarily hide any such .gitignore during the build, then restore it.
_HIDDEN_GITIGNORES=()
_dir="$(pwd)"
while [ "$_dir" != "/" ]; do
_dir="$(dirname "$_dir")"
if [ -f "$_dir/.gitignore" ] && grep -qx '\*' "$_dir/.gitignore" 2>/dev/null; then
mv "$_dir/.gitignore" "$_dir/.gitignore._twbuild"
_HIDDEN_GITIGNORES+=("$_dir/.gitignore")
fi
done
_restore_gitignores() {
for _gi in "${_HIDDEN_GITIGNORES[@]+"${_HIDDEN_GITIGNORES[@]}"}"; do
mv "${_gi}._twbuild" "$_gi" 2>/dev/null || true
done
}
trap _restore_gitignores EXIT
npm install
npm run build # outputs to studio/frontend/dist/
_restore_gitignores
trap - EXIT
# Validate CSS output -- catch truncated Tailwind builds before packaging
MAX_CSS_SIZE=$(find dist/assets -name '*.css' -exec wc -c {} + 2>/dev/null | sort -n | tail -1 | awk '{print $1}')
if [ -z "$MAX_CSS_SIZE" ]; then
echo "❌ ERROR: No CSS files were emitted into dist/assets."
echo " The frontend build may have failed silently."
exit 1
fi
if [ "$MAX_CSS_SIZE" -lt 100000 ]; then
echo "❌ ERROR: Largest CSS file is only $((MAX_CSS_SIZE / 1024))KB (expected >100KB)."
echo " Tailwind may not have scanned all source files."
echo " Check for .gitignore files blocking the Tailwind oxide scanner."
exit 1
fi
echo "✅ Frontend CSS validated (${MAX_CSS_SIZE} bytes)"
cd ../..
# 2. Clean old artifacts

119
install.ps1 Normal file
View file

@ -0,0 +1,119 @@
# Unsloth Studio Installer for Windows PowerShell
# Usage: irm https://raw.githubusercontent.com/unslothai/unsloth/main/install.ps1 | iex
# Local: Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass; .\install.ps1
function Install-UnslothStudio {
$ErrorActionPreference = "Stop"
$VenvName = "unsloth_studio"
$PythonVersion = "3.13"
Write-Host ""
Write-Host "========================================="
Write-Host " Unsloth Studio Installer (Windows)"
Write-Host "========================================="
Write-Host ""
# ── Helper: refresh PATH from registry (preserving current session entries) ──
function Refresh-SessionPath {
$machine = [System.Environment]::GetEnvironmentVariable("Path", "Machine")
$user = [System.Environment]::GetEnvironmentVariable("Path", "User")
$env:Path = "$machine;$user;$env:Path"
}
# ── Check winget ──
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Host "Error: winget is not available." -ForegroundColor Red
Write-Host " Install it from https://aka.ms/getwinget" -ForegroundColor Yellow
Write-Host " or install Python $PythonVersion and uv manually, then re-run." -ForegroundColor Yellow
return
}
# ── Install Python if no compatible version (3.11-3.13) found ──
$DetectedPythonVersion = ""
if (Get-Command python -ErrorAction SilentlyContinue) {
$pyVer = python --version 2>&1
if ($pyVer -match "Python (3\.1[1-3])\.\d+") {
Write-Host "==> Python already installed: $pyVer"
$DetectedPythonVersion = $Matches[1]
}
}
if (-not $DetectedPythonVersion) {
Write-Host "==> Installing Python ${PythonVersion}..."
winget install -e --id Python.Python.3.13 --accept-package-agreements --accept-source-agreements
Refresh-SessionPath
if ($LASTEXITCODE -ne 0) {
# winget returns non-zero for "already installed" -- only fail if python is truly missing
if (-not (Get-Command python -ErrorAction SilentlyContinue)) {
Write-Host "[ERROR] Python installation failed (exit code $LASTEXITCODE)" -ForegroundColor Red
return
}
}
$DetectedPythonVersion = $PythonVersion
}
# ── Install uv if not present ──
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
Write-Host "==> Installing uv package manager..."
winget install --id=astral-sh.uv -e --accept-package-agreements --accept-source-agreements
Refresh-SessionPath
# Fallback: if winget didn't put uv on PATH, try the PowerShell installer
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
Write-Host " Trying alternative uv installer..."
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Refresh-SessionPath
}
}
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
Write-Host "Error: uv could not be installed." -ForegroundColor Red
Write-Host " Install it from https://docs.astral.sh/uv/" -ForegroundColor Yellow
return
}
# ── Create venv (skip if it already exists and has a valid interpreter) ──
$VenvPython = Join-Path $VenvName "Scripts\python.exe"
if (-not (Test-Path $VenvPython)) {
if (Test-Path $VenvName) { Remove-Item -Recurse -Force $VenvName }
Write-Host "==> Creating Python ${DetectedPythonVersion} virtual environment (${VenvName})..."
uv venv $VenvName --python $DetectedPythonVersion
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] Failed to create virtual environment (exit code $LASTEXITCODE)" -ForegroundColor Red
return
}
} else {
Write-Host "==> Virtual environment ${VenvName} already exists, skipping creation."
}
# ── Install unsloth directly into the venv (no activation needed) ──
Write-Host "==> Installing unsloth (this may take a few minutes)..."
uv pip install --python $VenvPython unsloth --torch-backend=auto
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] Failed to install unsloth (exit code $LASTEXITCODE)" -ForegroundColor Red
return
}
# ── Run studio setup ──
# setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools,
# CUDA Toolkit, Node.js, and other dependencies automatically via winget.
Write-Host "==> Running unsloth studio setup..."
$UnslothExe = Join-Path $VenvName "Scripts\unsloth.exe"
& $UnslothExe studio setup
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] unsloth studio setup failed (exit code $LASTEXITCODE)" -ForegroundColor Red
return
}
Write-Host ""
Write-Host "========================================="
Write-Host " Unsloth Studio installed!"
Write-Host "========================================="
Write-Host ""
Write-Host " To launch, run:"
Write-Host ""
Write-Host " .\${VenvName}\Scripts\activate"
Write-Host " unsloth studio -H 0.0.0.0 -p 8888"
Write-Host ""
}
Install-UnslothStudio

212
install.sh Executable file
View file

@ -0,0 +1,212 @@
#!/bin/sh
# Unsloth Studio Installer
# Usage (curl): curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/install.sh | sh
# Usage (wget): wget -qO- https://raw.githubusercontent.com/unslothai/unsloth/main/install.sh | sh
set -e
VENV_NAME="unsloth_studio"
PYTHON_VERSION="3.13"
# ── Helper: download a URL to a file (supports curl and wget) ──
download() {
if command -v curl >/dev/null 2>&1; then
curl -LsSf "$1" -o "$2"
elif command -v wget >/dev/null 2>&1; then
wget -qO "$2" "$1"
else
echo "Error: neither curl nor wget found. Install one and re-run."
exit 1
fi
}
# ── Helper: check if a single package is available on the system ──
_is_pkg_installed() {
case "$1" in
build-essential) command -v gcc >/dev/null 2>&1 ;;
libcurl4-openssl-dev)
command -v dpkg >/dev/null 2>&1 && dpkg -s "$1" >/dev/null 2>&1 ;;
pciutils)
command -v lspci >/dev/null 2>&1 ;;
*) command -v "$1" >/dev/null 2>&1 ;;
esac
}
# ── Helper: install packages via apt, escalating to sudo only if needed ──
# Usage: _smart_apt_install pkg1 pkg2 pkg3 ...
_smart_apt_install() {
_PKGS="$*"
# Step 1: Try installing without sudo (works when already root)
apt-get update -y >/dev/null 2>&1 || true
apt-get install -y $_PKGS >/dev/null 2>&1 || true
# Step 2: Check which packages are still missing
_STILL_MISSING=""
for _pkg in $_PKGS; do
if ! _is_pkg_installed "$_pkg"; then
_STILL_MISSING="$_STILL_MISSING $_pkg"
fi
done
_STILL_MISSING=$(echo "$_STILL_MISSING" | sed 's/^ *//')
if [ -z "$_STILL_MISSING" ]; then
return 0
fi
# Step 3: Escalate -- need elevated permissions for remaining packages
if command -v sudo >/dev/null 2>&1; then
echo ""
echo " !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo " WARNING: We require sudo elevated permissions to install:"
echo " $_STILL_MISSING"
echo " If you accept, we'll run sudo now, and it'll prompt your password."
echo " !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo ""
printf " Accept? [Y/n] "
if [ -r /dev/tty ]; then
read -r REPLY </dev/tty || REPLY="y"
else
REPLY="y"
fi
case "$REPLY" in
[nN]*)
echo ""
echo " Please install these packages first, then re-run Unsloth Studio setup:"
echo " sudo apt-get update -y && sudo apt-get install -y $_STILL_MISSING"
exit 1
;;
*)
sudo apt-get update -y
sudo apt-get install -y $_STILL_MISSING
;;
esac
else
echo ""
echo " sudo is not available on this system."
echo " Please install these packages as root, then re-run Unsloth Studio setup:"
echo " apt-get update -y && apt-get install -y $_STILL_MISSING"
exit 1
fi
}
echo ""
echo "========================================="
echo " Unsloth Studio Installer"
echo "========================================="
echo ""
# ── Detect platform ──
OS="linux"
if [ "$(uname)" = "Darwin" ]; then
OS="macos"
elif grep -qi microsoft /proc/version 2>/dev/null; then
OS="wsl"
fi
echo "==> Platform: $OS"
# ── Check system dependencies ──
# cmake and git are needed by unsloth studio setup to build the GGUF inference
# engine (llama.cpp). build-essential and libcurl-dev are also needed on Linux.
MISSING=""
command -v cmake >/dev/null 2>&1 || MISSING="$MISSING cmake"
command -v git >/dev/null 2>&1 || MISSING="$MISSING git"
case "$OS" in
macos)
# Xcode Command Line Tools provide the C/C++ compiler
if ! xcode-select -p >/dev/null 2>&1; then
echo ""
echo "==> Xcode Command Line Tools are required."
echo " Installing (a system dialog will appear)..."
xcode-select --install 2>/dev/null || true
echo " After the installation completes, please re-run this script."
exit 1
fi
;;
linux|wsl)
# curl or wget is needed for downloads; check both
if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
MISSING="$MISSING curl"
fi
command -v gcc >/dev/null 2>&1 || MISSING="$MISSING build-essential"
# libcurl dev headers for llama.cpp HTTPS support
if command -v dpkg >/dev/null 2>&1; then
dpkg -s libcurl4-openssl-dev >/dev/null 2>&1 || MISSING="$MISSING libcurl4-openssl-dev"
fi
;;
esac
MISSING=$(echo "$MISSING" | sed 's/^ *//')
if [ -n "$MISSING" ]; then
echo ""
echo "==> Unsloth Studio needs these packages: $MISSING"
echo " These are needed to build the GGUF inference engine."
case "$OS" in
macos)
if ! command -v brew >/dev/null 2>&1; then
echo ""
echo " Homebrew is required to install them."
echo " Install Homebrew from https://brew.sh then re-run this script."
exit 1
fi
brew install $MISSING
;;
linux|wsl)
if command -v apt-get >/dev/null 2>&1; then
_smart_apt_install $MISSING
else
echo " apt-get is not available. Please install with your package manager:"
echo " $MISSING"
echo " Then re-run Unsloth Studio setup."
exit 1
fi
;;
esac
echo ""
else
echo "==> All system dependencies found."
fi
# ── Install uv ──
if ! command -v uv >/dev/null 2>&1; then
echo "==> Installing uv package manager..."
_uv_tmp=$(mktemp)
download "https://astral.sh/uv/install.sh" "$_uv_tmp"
sh "$_uv_tmp"
rm -f "$_uv_tmp"
if [ -f "$HOME/.local/bin/env" ]; then
. "$HOME/.local/bin/env"
fi
export PATH="$HOME/.local/bin:$PATH"
fi
# ── Create venv (skip if it already exists and has a valid interpreter) ──
if [ ! -x "$VENV_NAME/bin/python" ]; then
[ -e "$VENV_NAME" ] && rm -rf "$VENV_NAME"
echo "==> Creating Python ${PYTHON_VERSION} virtual environment (${VENV_NAME})..."
uv venv "$VENV_NAME" --python "$PYTHON_VERSION"
else
echo "==> Virtual environment ${VENV_NAME} already exists, skipping creation."
fi
# ── Install unsloth directly into the venv (no activation needed) ──
echo "==> Installing unsloth (this may take a few minutes)..."
uv pip install --python "$VENV_NAME/bin/python" unsloth --torch-backend=auto
# ── Run studio setup ──
echo "==> Running unsloth studio setup..."
"$VENV_NAME/bin/unsloth" studio setup </dev/null
echo ""
echo "========================================="
echo " Unsloth Studio installed!"
echo "========================================="
echo ""
echo " To launch, run:"
echo ""
echo " source ${VENV_NAME}/bin/activate"
echo " unsloth studio -H 0.0.0.0 -p 8888"
echo ""

View file

@ -45,7 +45,7 @@ from loggers import get_logger
logger = get_logger(__name__)
def get_colab_url(port: int = 8000) -> str:
def get_colab_url(port: int = 8888) -> str:
"""
Get the actual Colab proxy URL for a port.
"""
@ -60,7 +60,7 @@ def get_colab_url(port: int = 8000) -> str:
return f"http://localhost:{port}"
def show_link(port: int = 8000):
def show_link(port: int = 8888):
"""Display a styled clickable link to the UI."""
from IPython.display import display, HTML
@ -68,8 +68,8 @@ def show_link(port: int = 8000):
url = get_colab_url(port)
short_url = (
url[: url.index("-", url.index("8000-") + 5) + 1] + "..."
if "8000-" in url
url[: url.index("-", url.index(f"{port}-") + len(str(port)) + 1) + 1] + "..."
if f"{port}-" in url
else url
)
html = f"""
@ -96,7 +96,7 @@ def show_link(port: int = 8000):
display(HTML(html))
def start(port: int = 8000):
def start(port: int = 8888):
"""
Start Unsloth Studio server in Colab and display the URL.

View file

@ -772,6 +772,11 @@ class UnslothTrainer:
if self.should_stop:
return False
if full_finetuning:
# Enable training mode for full fine-tuning
# This ensures all model parameters are trainable; otherwise, they might be frozen.
self.model.for_training()
self._update_progress(status_message = "Model loaded successfully")
logger.info("Model loaded successfully")
return True

View file

@ -26,8 +26,9 @@ omegaconf
einx
pyloudnorm
openai-whisper
# uroman # 4.0 MB - romanization, no imports found
# MeCab # 19.9 MB - Japanese tokenizer, no imports found
uroman # 4.0 MB - used for Outetts.
MeCab # 19.9 MB - used for Outetts.
inflect # number-to-words, required by OuteTTS
loguru
flatten_dict
ffmpy

View file

@ -155,7 +155,7 @@ _shutdown_event = None
def run_server(
host: str = "0.0.0.0",
port: int = 8000,
port: int = 8888,
frontend_path: Path = Path(__file__).resolve().parent.parent / "frontend" / "dist",
silent: bool = False,
):
@ -224,13 +224,17 @@ def run_server(
if not silent:
display_host = _resolve_external_ip() if host == "0.0.0.0" else host
print("")
print("=" * 50)
print(f"🦥 Open your web browser, and enter http://localhost:{port}")
print("=" * 50)
print("")
print("=" * 50)
print(f"🦥 Unsloth Studio is running on port {port}")
print(f" Local: http://localhost:{port}")
print(f" External: http://{display_host}:{port}")
print(f" API: http://{display_host}:{port}/api")
print(f" Health: http://{display_host}:{port}/api/health")
print(f" Local Access: http://localhost:{port}")
print(f" Worldwide Web Address: http://{display_host}:{port}")
print(f" API: http://{display_host}:{port}/api")
print(f" Health: http://{display_host}:{port}/api/health")
print("=" * 50)
return app
@ -243,7 +247,7 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(description = "Run Unsloth UI Backend server")
parser.add_argument("--host", default = "0.0.0.0", help = "Host to bind to")
parser.add_argument("--port", type = int, default = 8000, help = "Port to bind to")
parser.add_argument("--port", type = int, default = 8888, help = "Port to bind to")
parser.add_argument(
"--frontend",
type = str,

View file

@ -630,7 +630,10 @@ _AUDIO_TOKEN_PATTERNS = {
"whisper": lambda tokens: "<|startoftranscript|>" in tokens,
"audio_vlm": lambda tokens: "<audio_soft_token>" in tokens,
"bicodec": lambda tokens: any(t.startswith("<|bicodec_") for t in tokens),
"dac": lambda tokens: "<|audio_start|>" in tokens and "<|audio_end|>" in tokens,
"dac": lambda tokens: "<|audio_start|>" in tokens
and "<|audio_end|>" in tokens
and "<|text_start|>" in tokens
and "<|text_end|>" in tokens,
"snac": lambda tokens: sum(1 for t in tokens if t.startswith("<custom_token_"))
> 10000,
}

View file

@ -15,10 +15,16 @@ import { AppProvider } from "../provider";
const CHAT_ONLY_ALLOWED = new Set(["/", "/chat", "/login", "/signup", "/change-password"]);
function isChatOnlyAllowed(pathname: string): boolean {
if (CHAT_ONLY_ALLOWED.has(pathname)) return true;
if (pathname === "/data-recipes" || pathname.startsWith("/data-recipes/")) return true;
return false;
}
export const Route = createRootRoute({
beforeLoad: ({ location }) => {
const chatOnly = usePlatformStore.getState().isChatOnly();
if (chatOnly && !CHAT_ONLY_ALLOWED.has(location.pathname)) {
if (chatOnly && !isChatOnlyAllowed(location.pathname)) {
throw redirect({ to: "/chat" });
}
},

View file

@ -93,7 +93,7 @@ export function Navbar() {
const disabledByTraining =
isTrainingRunning && item.href !== "/studio";
const disabledByDevice =
chatOnly && item.href !== "/chat";
chatOnly && item.href !== "/chat" && item.href !== "/data-recipes";
if (!item.enabled || disabledByTraining || disabledByDevice) {
return (
<span
@ -209,7 +209,7 @@ export function Navbar() {
</div>
{/* Right: mobile */}
<div className="flex items-center gap-2 md:hidden">
<div className="col-start-3 flex items-center gap-2 justify-self-end md:hidden">
{tourId ? (
<button
type="button"
@ -240,13 +240,14 @@ export function Navbar() {
const disabledByTraining =
isTrainingRunning && item.href !== "/studio";
const disabledByDevice =
chatOnly && item.href !== "/chat";
chatOnly && item.href !== "/chat" && item.href !== "/data-recipes";
if (disabledByTraining || disabledByDevice) {
return (
<span
key={item.href}
className="rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground/40 cursor-not-allowed"
className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground/40 cursor-not-allowed"
>
<HugeiconsIcon icon={item.icon} className="size-4" />
{item.label}
</span>
);
@ -257,12 +258,13 @@ export function Navbar() {
to={item.href}
onClick={() => setMobileOpen(false)}
className={cn(
"rounded-md border px-3 py-2 text-sm font-medium",
"flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium",
active
? "border-foreground bg-foreground text-background"
: "border-border text-foreground hover:bg-accent",
)}
>
<HugeiconsIcon icon={item.icon} className="size-4" />
{item.label}
</Link>
);
@ -271,23 +273,33 @@ export function Navbar() {
href="https://unsloth.ai/docs"
target="_blank"
rel="noopener noreferrer"
className="mt-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-foreground hover:bg-accent"
className="mt-2 flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-foreground hover:bg-accent"
onClick={() => setMobileOpen(false)}
>
<HugeiconsIcon icon={Book03Icon} className="size-4" />
Learn more (Docs)
</a>
{tourId ? (
<button
type="button"
className="rounded-md border border-border px-3 py-2 text-left text-sm font-medium text-foreground hover:bg-accent"
className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-left text-sm font-medium text-foreground hover:bg-accent"
onClick={() => {
openTour();
setMobileOpen(false);
}}
>
<HugeiconsIcon icon={CursorInfo02Icon} className="size-4" />
Start tour
</button>
) : null}
<div className="mt-2 flex items-center justify-between rounded-md border border-border px-3 py-2">
<span className="text-sm font-medium text-foreground">Theme</span>
<AnimatedThemeToggler
className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground [&_svg]:size-4"
title="Toggle theme"
aria-label="Toggle theme"
/>
</div>
</div>
</SheetContent>
</Sheet>

View file

@ -189,6 +189,7 @@ export {
DialogHeader,
DialogOverlay,
DialogPortal,
DialogPortalContainerContext,
DialogTitle,
DialogTrigger,
};

View file

@ -3,8 +3,10 @@
import { Dialog as SheetPrimitive } from "radix-ui";
import type * as React from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { DialogPortalContainerContext } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { Cancel01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
@ -69,6 +71,7 @@ function SheetContent({
overlayClassName?: string;
overlayPosition?: "fixed" | "absolute";
}) {
const [contentEl, setContentEl] = useState<HTMLDivElement | null>(null);
return (
<SheetPortal container={container ?? undefined}>
<SheetOverlay
@ -76,6 +79,7 @@ function SheetContent({
position={overlayPosition ?? position}
/>
<SheetPrimitive.Content
ref={setContentEl}
data-slot="sheet-content"
data-side={side}
className={cn(
@ -85,7 +89,9 @@ function SheetContent({
)}
{...props}
>
{children}
<DialogPortalContainerContext.Provider value={contentEl}>
{children}
</DialogPortalContainerContext.Provider>
{showCloseButton && (
<SheetPrimitive.Close data-slot="sheet-close" asChild>
<Button

View file

@ -768,6 +768,7 @@ export function ChatPage(): ReactElement {
<ChatSettingsPanel
open={settingsOpen}
onOpenChange={setSettingsOpen}
params={inferenceParams}
onParamsChange={setInferenceParams}
autoTitle={autoTitle}

View file

@ -21,6 +21,14 @@ import {
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { AnimatePresence, motion } from "motion/react";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { useIsMobile } from "@/hooks/use-mobile";
import type { ReactNode } from "react";
import { useState } from "react";
import {
@ -151,6 +159,7 @@ function CollapsibleSection({
interface ChatSettingsPanelProps {
open: boolean;
onOpenChange?: (open: boolean) => void;
params: InferenceParams;
onParamsChange: (params: InferenceParams) => void;
autoTitle: boolean;
@ -160,12 +169,14 @@ interface ChatSettingsPanelProps {
export function ChatSettingsPanel({
open,
onOpenChange,
params,
onParamsChange,
autoTitle,
onAutoTitleChange,
onReloadModel,
}: ChatSettingsPanelProps) {
const isMobile = useIsMobile();
const isGguf = useChatRuntimeStore((s) => s.activeGgufVariant) != null;
const ggufContextLength = useChatRuntimeStore((s) => s.ggufContextLength);
const kvCacheDtype = useChatRuntimeStore((s) => s.kvCacheDtype);
@ -214,24 +225,21 @@ export function ChatSettingsPanel({
}
}
return (
<aside
className={`shrink-0 self-start h-[calc(100%-0.875rem)] overflow-hidden bg-muted/70 rounded-2xl corner-squircle transition-[width] duration-200 ease-linear ${open ? "w-[17rem] border-l border-sidebar-border/70" : "w-0"}`}
>
<div className="flex h-full w-[17rem] flex-col">
<div className="flex items-center gap-2 px-4 py-3">
<HugeiconsIcon
icon={PencilEdit01Icon}
className="size-4 text-muted-foreground/70"
/>
<span className="flex-1 text-base font-semibold tracking-tight">
Configuration
</span>
</div>
const settingsContent = (
<>
<div className="flex items-center gap-2 px-4 py-3">
<HugeiconsIcon
icon={PencilEdit01Icon}
className="size-4 text-muted-foreground/70"
/>
<span className="flex-1 text-base font-semibold tracking-tight">
Configuration
</span>
</div>
<div className="flex-1 overflow-y-auto px-1.5">
{/* mt-4 matches the Playground sidebar gap (SidebarHeader py-3 + SidebarGroup pt-1) */}
<div className="mt-4 px-2 pb-3">
<div className="flex-1 overflow-y-auto px-1.5">
{/* mt-4 matches the Playground sidebar gap (SidebarHeader py-3 + SidebarGroup pt-1) */}
<div className="mt-4 px-2 pb-3">
<div className="flex items-center gap-2">
<Select value={activePreset} onValueChange={applyPreset}>
<SelectTrigger className="h-8 flex-1 corner-squircle text-xs">
@ -434,7 +442,28 @@ export function ChatSettingsPanel({
<ChatTemplateSection onReloadModel={onReloadModel} />
</div>
</div>
</>
);
if (isMobile) {
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right" className="w-[18rem] p-0">
<SheetHeader className="sr-only">
<SheetTitle>Configuration</SheetTitle>
<SheetDescription>Chat inference settings</SheetDescription>
</SheetHeader>
<div className="flex h-full flex-col">{settingsContent}</div>
</SheetContent>
</Sheet>
);
}
return (
<aside
className={`shrink-0 self-start h-[calc(100%-0.875rem)] overflow-hidden bg-muted/70 rounded-2xl corner-squircle transition-[width] duration-200 ease-linear ${open ? "w-[17rem] border-l border-sidebar-border/70" : "w-0"}`}
>
<div className="flex h-full w-[17rem] flex-col">{settingsContent}</div>
</aside>
);
}

View file

@ -17,11 +17,11 @@ export default defineConfig({
allowedHosts: true,
proxy: {
"/api": {
target: "http://127.0.0.1:8000",
target: "http://127.0.0.1:8888",
changeOrigin: true,
},
"/v1": {
target: "http://127.0.0.1:8000",
target: "http://127.0.0.1:8888",
changeOrigin: true,
},
"/seed/inspect": {

View file

@ -855,10 +855,31 @@ if ($IsPipInstall) {
Write-Host "[INFO] Frontend source changed since last build -- rebuilding..." -ForegroundColor Yellow
}
}
$NeedFrontendBuild = $true
if ($NeedFrontendBuild -and -not $IsPipInstall) {
Write-Host ""
Write-Host "Building frontend..." -ForegroundColor Cyan
# ── Tailwind v4 .gitignore workaround ──
# Tailwind v4's oxide scanner respects .gitignore in parent directories.
# Python venvs create a .gitignore with "*" (ignore everything), which
# prevents Tailwind from scanning .tsx source files for class names.
# Temporarily hide any such .gitignore during the build, then restore it.
$HiddenGitignores = @()
$WalkDir = (Get-Item $FrontendDir).Parent.FullName
while ($WalkDir -and $WalkDir -ne [System.IO.Path]::GetPathRoot($WalkDir)) {
$gi = Join-Path $WalkDir ".gitignore"
if (Test-Path $gi) {
$content = Get-Content $gi -Raw -ErrorAction SilentlyContinue
if ($content -and ($content.Trim() -match '^\*$')) {
$hidden = "$gi._twbuild"
Rename-Item -Path $gi -NewName (Split-Path $hidden -Leaf) -Force
$HiddenGitignores += $gi
Write-Host " [INFO] Temporarily hiding $gi (venv .gitignore blocks Tailwind scanner)" -ForegroundColor DarkGray
}
}
$WalkDir = Split-Path $WalkDir -Parent
}
# npm writes warnings to stderr; lower ErrorActionPreference so PS doesn't
# treat them as terminating errors (same pattern as the pip section below).
$prevEAP_npm = $ErrorActionPreference
@ -868,6 +889,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) {
if ($LASTEXITCODE -ne 0) {
Pop-Location
$ErrorActionPreference = $prevEAP_npm
foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue }
Write-Host "[ERROR] npm install failed (exit code $LASTEXITCODE)" -ForegroundColor Red
Write-Host " Try running 'npm install' manually in frontend/ to see errors" -ForegroundColor Yellow
exit 1
@ -876,12 +898,27 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) {
if ($LASTEXITCODE -ne 0) {
Pop-Location
$ErrorActionPreference = $prevEAP_npm
foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue }
Write-Host "[ERROR] npm run build failed (exit code $LASTEXITCODE)" -ForegroundColor Red
exit 1
}
Pop-Location
$ErrorActionPreference = $prevEAP_npm
Write-Host "[OK] Frontend built to frontend/dist" -ForegroundColor Green
# ── Restore hidden .gitignore files ──
foreach ($gi in $HiddenGitignores) {
Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue
}
# ── Validate CSS output ──
$CssFiles = Get-ChildItem (Join-Path $DistDir "assets") -Filter "*.css" -ErrorAction SilentlyContinue
$MaxCssSize = ($CssFiles | Measure-Object -Property Length -Maximum).Maximum
if ($MaxCssSize -lt 100000) {
Write-Host "[WARN] Largest CSS file is only $([math]::Round($MaxCssSize / 1024))KB -- Tailwind may not have scanned all source files." -ForegroundColor Yellow
Write-Host " Expected >100KB. Check for .gitignore files blocking the Tailwind oxide scanner." -ForegroundColor Yellow
} else {
Write-Host "[OK] Frontend built to frontend/dist (CSS: $([math]::Round($MaxCssSize / 1024))KB)" -ForegroundColor Green
}
}
if (Test-Path $OxcValidatorDir) {
@ -1374,6 +1411,6 @@ Write-Host "+===============================================+" -ForegroundColor
Write-Host "| Setup Complete! |" -ForegroundColor Green
Write-Host "| |" -ForegroundColor Green
Write-Host "| Launch with: |" -ForegroundColor Green
Write-Host "| unsloth studio -H 0.0.0.0 -p 8000 |" -ForegroundColor Green
Write-Host "| unsloth studio -H 0.0.0.0 -p 8888 |" -ForegroundColor Green
Write-Host "| |" -ForegroundColor Green
Write-Host "+===============================================+" -ForegroundColor Green

View file

@ -59,7 +59,6 @@ if [ -d "$SCRIPT_DIR/frontend/dist" ]; then
_NEED_FRONTEND_BUILD=false
fi
fi
_NEED_FRONTEND_BUILD=true
if [ "$_NEED_FRONTEND_BUILD" = false ]; then
echo "✅ Frontend already built and up to date -- skipping Node/npm check."
else
@ -160,6 +159,16 @@ run_quiet "npm run build" npm run build
_restore_gitignores
trap - EXIT
# Validate CSS output -- catch truncated Tailwind builds
_MAX_CSS=$(find "$SCRIPT_DIR/frontend/dist/assets" -name '*.css' -exec wc -c {} + 2>/dev/null | sort -n | tail -1 | awk '{print $1}')
if [ -z "$_MAX_CSS" ]; then
echo "⚠️ WARNING: No CSS files were emitted. The frontend build may have failed."
elif [ "$_MAX_CSS" -lt 100000 ]; then
echo "⚠️ WARNING: Largest CSS file is only $((_MAX_CSS / 1024))KB (expected >100KB)."
echo " Tailwind may not have scanned all source files. Check for .gitignore interference."
fi
cd "$SCRIPT_DIR"
echo "✅ Frontend built to frontend/dist"
@ -308,10 +317,59 @@ echo "✅ Transformers 5.x pre-installed to $VENV_T5_DIR/"
if grep -qi microsoft /proc/version 2>/dev/null; then
echo ""
echo "⚠️ WSL detected -- installing build dependencies for GGUF export..."
echo " You may be prompted for your password."
sudo apt-get update -y
sudo apt-get install -y build-essential cmake curl git libcurl4-openssl-dev
echo "✅ GGUF build dependencies installed"
_GGUF_DEPS="pciutils build-essential cmake curl git libcurl4-openssl-dev"
# Try without sudo first (works when already root)
apt-get update -y >/dev/null 2>&1 || true
apt-get install -y $_GGUF_DEPS >/dev/null 2>&1 || true
# Check which packages are still missing
_STILL_MISSING=""
for _pkg in $_GGUF_DEPS; do
case "$_pkg" in
build-essential) command -v gcc >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;;
pciutils) command -v lspci >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;;
libcurl4-openssl-dev) dpkg -s "$_pkg" >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;;
*) command -v "$_pkg" >/dev/null 2>&1 || _STILL_MISSING="$_STILL_MISSING $_pkg" ;;
esac
done
_STILL_MISSING=$(echo "$_STILL_MISSING" | sed 's/^ *//')
if [ -z "$_STILL_MISSING" ]; then
echo "✅ GGUF build dependencies installed"
elif command -v sudo >/dev/null 2>&1; then
echo ""
echo " !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo " WARNING: We require sudo elevated permissions to install:"
echo " $_STILL_MISSING"
echo " If you accept, we'll run sudo now, and it'll prompt your password."
echo " !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo ""
printf " Accept? [Y/n] "
if [ -r /dev/tty ]; then
read -r REPLY </dev/tty || REPLY="y"
else
REPLY="y"
fi
case "$REPLY" in
[nN]*)
echo ""
echo " Please install these packages first, then re-run Unsloth Studio setup:"
echo " sudo apt-get update -y && sudo apt-get install -y $_STILL_MISSING"
_SKIP_GGUF_BUILD=true
;;
*)
sudo apt-get update -y
sudo apt-get install -y $_STILL_MISSING
echo "✅ GGUF build dependencies installed"
;;
esac
else
echo " sudo is not available on this system."
echo " Please install as root, then re-run setup:"
echo " apt-get install -y $_STILL_MISSING"
_SKIP_GGUF_BUILD=true
fi
fi
# ── 8. Build llama.cpp binaries for GGUF inference + export ──
@ -324,6 +382,11 @@ UNSLOTH_HOME="$HOME/.unsloth"
mkdir -p "$UNSLOTH_HOME"
LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp"
LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server"
if [ "${_SKIP_GGUF_BUILD:-}" = true ]; then
echo ""
echo "Skipping llama-server build (missing dependencies)"
echo " Install the missing packages and re-run setup to enable GGUF inference."
else
rm -rf "$LLAMA_CPP_DIR"
{
# Check prerequisites
@ -442,6 +505,7 @@ rm -rf "$LLAMA_CPP_DIR"
fi
fi
}
fi # end _SKIP_GGUF_BUILD check
echo ""
if [ "$IS_COLAB" = true ]; then
@ -460,6 +524,6 @@ else
echo "╠══════════════════════════════════════╣"
echo "║ Launch with: ║"
echo "║ ║"
echo "║ unsloth studio -H 0.0.0.0 -p 8000 ║"
echo "║ unsloth studio -H 0.0.0.0 -p 8888 ║"
echo "╚══════════════════════════════════════╝"
fi

View file

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
__version__ = "2026.3.7"
__version__ = "2026.3.8"
__all__ = [
"SUPPORTS_BFLOAT16",

View file

@ -75,7 +75,7 @@ def _find_setup_script() -> Optional[Path]:
@studio_app.callback(invoke_without_command = True)
def studio_default(
ctx: typer.Context,
port: int = typer.Option(8000, "--port", "-p"),
port: int = typer.Option(8888, "--port", "-p"),
host: str = typer.Option("0.0.0.0", "--host", "-H"),
frontend: Optional[Path] = typer.Option(None, "--frontend", "-f"),
silent: bool = typer.Option(False, "--silent", "-q"),
@ -93,7 +93,7 @@ def studio_default(
run_py = _find_run_py()
if studio_python and run_py:
if not silent:
typer.echo("Launching with studio venv...")
typer.echo("Launching Unsloth Studio... Please wait...")
args = [
str(studio_python),
str(run_py),

View file

@ -12,7 +12,7 @@ import typer
def ui(
port: int = typer.Option(
8000, "--port", "-p", help = "Port to run the UI server on."
8888, "--port", "-p", help = "Port to run the UI server on."
),
host: str = typer.Option(
"0.0.0.0", "--host", "-H", help = "Host address to bind to."
@ -40,7 +40,7 @@ def ui(
run_py = _find_run_py()
if studio_python and run_py:
if not silent:
typer.echo("Launching with studio venv...")
typer.echo("Launching Unsloth Studio... Please wait...")
args = [
str(studio_python),
str(run_py),