Commit graph

318 commits

Author SHA1 Message Date
JoshuaL3000
08853803d9
Update README to include Intel XPU support description for unsloth studio (#9250) 2026-08-19 04:46:18 -07:00
Michael Han
6371f46a99
Update README.md 2026-08-18 01:50:44 -07:00
Maheswar Kumar
ea687ef520
studio: add settings-managed LAN access (#8951)
* studio: add settings-managed LAN access

Studio binds 127.0.0.1, so reaching it from a phone on the same Wi-Fi meant
relaunching with -H 0.0.0.0, which the desktop app cannot do at all. Settings >
API keys > LAN access now adds a second uvicorn listener over the running app,
bound to each detected non-loopback IPv4 at the same port, on the primary
server's event loop with lifespan="off". The loopback socket is untouched, so
the desktop app keeps working while it is on.

lan_access.py owns the listener: address detection, binding, and a teardown that
waits on the sockets rather than on serve(), since uvicorn closes the passed
sockets at the top of shutdown and only then drains in-flight responses. Waiting
on the serve task made a Stop pressed from a LAN device wait out its own
response for the full timeout.

utils/lan_access_settings.py holds the launch policy and the persisted
lan_access_auto_start preference. Start is blocked on Colab, on a --secure
launch (which forces the loopback bind so the raw port is never exposed), on a
launch that already binds the network, and until the seeded admin password has
been changed.

main.py's desktop SPA gate now opens for requests arriving on a LAN listener
socket, identified by scope["server"] rather than any client header, so a
desktop api-only backend serves its packaged web UI over LAN without changing
the local api-only surface. host_policy tracks the LAN listener alongside the
Cloudflare tunnel, so turning it on suspends the loopback stdio-MCP default.
The startup banner reports the bound addresses when a persisted auto-start has
already brought the listener up, instead of claiming loopback only.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: keep LAN listener ownership until the port is confirmed closed

A stop that timed out dropped every reference to the server and sockets before
returning False, so the wait could never be retried and a second stop found
_server None, reported success, and cleared both the stop_timed_out error and
the beyond-loopback trust flag while the port could still be accepting. The
references are now retained on that path, so the status keeps offering Stop and
a retry waits on the same sockets.

start_lan_access also marks the LAN connector active before the listener can
accept, rolling back if startup fails. A request served between the socket
accepting and the flag being set would otherwise read the loopback-only stdio
MCP default in core/inference/mcp_client.py.

* studio: keep the LAN trust flag under the listener lock

start_lan_access and stop_lan_access are sync routes, so FastAPI runs them in
separate worker threads. Publishing the beyond-loopback flag from those callers
left it outside lan_access._lock: a stop that observed no server could set the
flag false between a concurrent start setting it true and that start acquiring
the lock, leaving a live LAN listener while remote_connector_active() read
false and core/inference/mcp_client.py still allowed the loopback-default stdio
MCP transport.

The flag now changes only where the listener state does, under the same lock:
start_lan_listener sets it before the socket can accept and rolls it back if
startup fails, and _release_listener_state clears it with the references.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: enumerate interfaces for LAN access and hold ownership through loop shutdown

detect_lan_addresses only had the route to 8.8.8.8 and a hostname lookup. The
probe yields one route-selected source address and hostname resolution is not an
enumeration, so a multihomed host lost its second adapter and an isolated LAN
with no default route reported no_lan_address while having a usable address, the
Linux name-to-127.0.1.1 mapping making it worse. It now enumerates IPv4
addresses on every interface that is up via psutil, keeping the default-route
address first so it stays the shown URL, and falls back to the hostname lookup
only when psutil is unavailable.

stop_lan_listener no longer releases ownership on the event-loop branch.
/api/shutdown reaches it from a task on the serving loop, and uvicorn cannot
close the sockets until that loop is free, which _graceful_shutdown then holds
for seconds while it stops the inference, export and training subprocesses. Only
_bound_addresses is dropped there, so the frontend gate closes at once while the
sockets and the beyond-loopback trust flag stay owned until they really close.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: hold LAN trust until the stopped listener's requests drain

Closing the listening sockets stops new connections, but uvicorn then drains the
ones it already accepted. Clearing the beyond-loopback flag at that point let a
LAN request still executing reach stdio_mcp_enabled() in
core/inference/mcp_client.py and be treated as loopback-only, so a remote caller
could spawn stdio MCP subprocesses under the local default. Stopping LAN access
while a long inference from a phone is in flight is an ordinary way to hit it.

The confirmed-close path now releases the listener references without the flag
and hands it to a drain watcher, which clears it once server_state.connections
empties. The watcher leaves the flag active if the connections never drain, and
skips the clear when a new listener has since taken ownership. The Stop response
itself still returns as soon as the sockets close.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: derive the LAN trust flag from listener and drain ownership

A second stop arriving after the first closed the sockets but while accepted
requests were still draining found _server already None and released the flag
outright, so those still-remote requests passed the loopback-default stdio MCP
gate. The idempotent stop path bypassed the drain watcher entirely.

The flag is no longer assigned by any path. _sync_lan_trust publishes it from
the authoritative state, a live listener or any stopped listener still draining,
counted by _pending_drains and taken under the same lock as the references. A
watcher whose connections never drain keeps its count rather than releasing, so
the flag fails closed. start_lan_listener still raises it explicitly before the
socket can accept.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: release bound sockets when the listener cannot be scheduled

asyncio.run_coroutine_threadsafe raises RuntimeError("Event loop is closed") if
the primary loop closes between _server_loop validating it and the schedule
call. That escaped past the failure cleanup with every socket already bound and
the trust flag already raised, so the port stayed listening with nothing serving
it, status reported no listener, later starts hit bind_failed, and the flag
stayed on until the process exited.

The cleanup is now shared by both start failure paths in _fail_start, which
closes the sockets, resyncs the trust flag from ownership and records
listener_start_failed. The unscheduled coroutine is closed so the failure does
not also emit a never-awaited warning.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: unblock a loop-side LAN stop and release the listener when its loop ends

start_lan_listener holds _lock while waiting for the serving loop to run
serve(). /api/shutdown reaches stop_lan_listener from a task on that same loop,
so blocking on the lock left the two waiting each other out: the loop could not
advance the scheduled listener, the start burned its full timeout and shutdown
stalled with it. A stop running on the serving loop now takes the lock without
blocking and reports an unconfirmed stop instead.

The event-loop branch keeps ownership so uvicorn can close the sockets once the
loop is free, but nothing released it afterwards. An embedded host that calls
run_server again in-process therefore saw a stale _server, reported the previous
addresses as online and never bound a new listener. run.py's server thread now
releases the listener in the same finally that closes the loop, which reaches
the loop-gone path and drops the sockets, references and trust flag.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: advertise only reachable LAN origins

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Etherll <61019402+Etherll@users.noreply.github.com>
2026-08-18 04:32:59 +03:00
Michael Han
5bb8a6f5bf
Update README.md 2026-08-17 03:17:55 -07:00
Michael Han
c9cda7bf32
Update README.md 2026-08-16 22:18:20 -07:00
yzxcj797
6f443b5ccd
docs: fix dead Linux .deb download link (#8891)
Some checks failed
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
Windows Unsloth GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Windows Unsloth GGUF CI / GGUF inference smoke (API, tools, vision) (push) Waiting to run
Windows Unsloth UI CI / Chat UI Tests (push) Waiting to run
Windows Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Windows Application Control CI / installer survives a denied unsloth.exe (push) Waiting to run
Workflow trigger lint / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Waiting to run
Local Agent Guides CI / resume (pi) (push) Has been cancelled
Local Agent Guides CI / prompt-cache (gemma-3-270m) (push) Has been cancelled
Kaggle T4 Studio GPU CI / gate (push) Has been cancelled
Local Agent Guides CI / connection (claude) (push) Has been cancelled
Local Agent Guides CI / connection (codex) (push) Has been cancelled
Local Agent Guides CI / connection (hermes) (push) Has been cancelled
Local Agent Guides CI / connection (openclaw) (push) Has been cancelled
Local Agent Guides CI / connection (opencode) (push) Has been cancelled
Local Agent Guides CI / connection (pi) (push) Has been cancelled
Local Agent Guides CI / file-edit (claude) (push) Has been cancelled
Local Agent Guides CI / file-edit (codex) (push) Has been cancelled
Local Agent Guides CI / file-edit (hermes) (push) Has been cancelled
Local Agent Guides CI / file-edit (openclaw) (push) Has been cancelled
Local Agent Guides CI / file-edit (opencode) (push) Has been cancelled
Local Agent Guides CI / file-edit (pi) (push) Has been cancelled
Local Agent Guides CI / resume (claude) (push) Has been cancelled
Local Agent Guides CI / resume (codex) (push) Has been cancelled
Local Agent Guides CI / resume (opencode) (push) Has been cancelled
Kaggle T4 Studio GPU CI / Studio GPU smoke (push) Has been cancelled
2026-08-14 23:28:27 -03:00
Daniel Han
ba466ca095 Update README.md 2026-08-14 08:39:36 -07:00
Michael Han
72e72572f8
Update README.md 2026-08-13 05:37:55 -07:00
Daniel Han
d5a2160ef8
Say ROCm does not cover RDNA 1 instead of advising a fix that cannot work (#8577)
* Say ROCm does not cover RDNA 1 instead of advising an impossible fix

An RX 5700 XT (Navi 10, gfx1010, RDNA 1) correctly lands on CPU PyTorch:
AMD publishes Windows torch indexes for gfx103X, gfx110X, gfx1150, gfx1151
and gfx120X, and there is no gfx101X index. Because the name-inference table
covers only arches that have wheels, the arch stayed null and the installer
fell into the "arch unknown" arm, which tells the user to install the HIP SDK
or set UNSLOTH_ROCM_GFX_ARCH. Neither can work: UNSLOTH_ROCM_GFX_ARCH=gfx1010
lands on the unmapped-arch path and returns CPU anyway.

Add a separate name lookup for AMD generations ROCm PyTorch does not cover,
read only to word the report. Product names come from LLVM's AMDGPU GFX10.1
processor table. The lookup never sets the arch the installers route on, so
CPU fallback is reached by exactly the same path as before.

Mirrored across install.ps1, studio/setup.ps1, install.sh, studio/setup.sh
and studio/install_python_stack.py so every install path agrees.

* Point pre-RDNA 2 AMD users at the Vulkan llama.cpp path

The previous commit stopped at "ROCm does not cover this GPU", which is true
and still a dead end. There is a working path: llama.cpp's Vulkan bundle drives
these cards, which is how #8458's reporter got an RX 580 running and how LM
Studio drives the same hardware.

Nothing routes these users there automatically. _should_auto_vulkan_for_amd_windows
opens with `active = _active_rocm_gfx_target(host); if not active: return False`,
and a pre-RDNA 2 card resolves to no gfx target at all, so the Windows auto-Vulkan
fallback structurally cannot fire for exactly the cards that need it. The
environment variable is their only route, so the message now names it.

Two things about that advice are load-bearing and both are tested:

- The current spelling, UNSLOTH_LLAMA_CPP_BACKEND=vulkan. The legacy
  UNSLOTH_FORCE_VULKAN still works but force_vulkan_requested() resolves the new
  variable first and consults the legacy one only when the new one is absent or
  unparseable, deliberately, so =hip stays a real opt-out that a stale legacy
  variable cannot overrule. New text must not spread the legacy name.
- WHEN to set it. The variable picks the llama.cpp bundle at install/download
  time; nothing reads it at runtime to choose a binary. #8458's reporter set it
  after installing, saw no change, and only a clean reinstall worked. Advice that
  names the variable without naming the moment is worse than none.

Also adds Polaris 10/20/30 (RX 470/480/570/580/590, gfx803) to the
messaging-only table so #8458's card gets the right message. gfx803 stays out of
_GFX_TO_AMD_INDEX_ARCH and every supported-arch table, and routing is untouched;
a test pins that directly. Polaris 11/12 (RX 460/550/560) is left out because
gfx803 vs gfx804 could not be confirmed for that die, and this table is only
worth having while it never guesses.

"RX 570" is a prefix of "RX 5700" and "RX 550" of "RX 5500". Python and
PowerShell carry (?!0) lookahead guards; POSIX `case` has no lookahead, so in
install.sh and studio/setup.sh correctness rests on arm order and the RDNA 1
arms come first. That order is now documented and asserted, and the shipped
arms are evaluated in a real shell rather than checked by eye.

README leads with the current spelling, since the installer now names a variable
and the README is where users check it.

Finally, test_cpu_index_note_respects_explicit_pin asserted a pin check appeared
within a character window before a note. That is a budget on intervening source,
not the ordering property it is for, and this work had already pushed it from
400 to 1400. It now walks the enclosing if/elif chain by indentation, so it
tests order and has no distance left to re-tune.

* Stop the HIP SDK arm outranking the pre-RDNA 2 message, and fix its advice

An RDNA 1 or Polaris user who already installed the HIP SDK never saw the new
message: the $HipSdkInstalled arm sits earlier in the chain and told them the
ROCm compute driver was missing, which is the impossible remediation this change
exists to remove, and those users installed the SDK because the old advice said
to. Guard that arm, plus the matching CPU-hint arm in install.ps1, on the
unsupported arch.

The Vulkan setter was printed as UNSLOTH_LLAMA_CPP_BACKEND=vulkan by the two
PowerShell installers and by the Windows-only branch in install_python_stack.py.
PowerShell parses that as a command name, so a user who pastes it sets nothing
and the next install picks the same CPU bundle. Print
$env:UNSLOTH_LLAMA_CPP_BACKEND = "vulkan" there, as the README already does.

The arms also said PyTorch training runs on CPU on these GPUs. It does not: with
no CUDA or XPU accelerator, unsloth raises NotImplementedError at import, which
is why studio/setup.sh already tells its other CPU-torch hosts that training and
GPU inference are unavailable. Say the same thing here.

README: Vega 20 (Radeon VII, MI50, gfx906) is older than RDNA 2 and does have a
ROCm PyTorch path (install.sh routes it to rocm6.3), so name Polaris and RDNA 1
instead of every pre-RDNA 2 AMD GPU.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Guard the README gfx906 carve-out against any spelling of the cutoff

The ban was on one exact literal, so "every AMD GPU older than RDNA 2"
passed while contradicting the Vega 20 carve-out two sentences later.
Match the phrase family instead, and assert the group is named by its
members (Polaris, RDNA 1) rather than by a generation cutoff.

* Tighten the comments added by this PR

Comments only, no code or user-facing string touched. Every reason a
guard exists is kept, just said in fewer lines.

* Keep the note on why the rationale sits above the arm

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: initialise the unsupported-arch state outside the AMD detection block

The ROCm summary reads $script:ROCmUnsupportedGfxArch unconditionally, but the
assignment sat inside `if (-not $HasNvidiaSmi)`, so an NVIDIA host never
defined it and a caller's Set-StrictMode turned the summary into an aborting
undefined-variable error. ROCmGfxArch beside it was always initialised at top
level; this one was not.

The guard asks the PowerShell parser whether any assignment is unnested rather
than comparing line numbers: the file has three -not $HasNvidiaSmi blocks, so
an ordering check picks the wrong one and passes for the wrong reason.

* Name the remaining RDNA 1 boards, diagnose the KFD-only host, and keep mixed AMD hosts on the arch-unknown advice

Adds the Navi 10 / Navi 14 professional boards LLVM's processor table omits
(Radeon Pro W5700/W5700X -> gfx1010, Pro W5500/W5500M/W5300M and RX 5300/5300M
-> gfx1012) to all five copies of the unsupported-name table. Each mapping comes
from libdrm data/amdgpu.ids read against pci.ids and the kernel amdgpu PCI table,
not from a guess; the tables still route nothing.

studio/setup.sh's KFD sysfs fallback detects the GPU without rocminfo or amd-smi,
so it left the marketing name empty and the report fell through to a plain
AMD ROCm line on a host with no ROCm. It now reads lspci for that report only,
never writing it back into the name the supported table and --rocm-gfx key on.

install.ps1's WMI fallback classifies adapter 0 only, so a host pairing an
RX 5700 with an RX 7900 was told nothing could enable ROCm, which is false there.
The verdict is now withheld when another adapter is covered, leaving the
arch-unknown advice that does apply. install_python_stack.py and studio/setup.ps1
already scored every adapter.

* Scope the uncovered-arch verdict to the card it names, and name the boards it was missing

A host is not one GPU. On a box pairing an uncovered card with one that has
wheels -- an RX 580 beside an RX 7900 XTX, or beside an Instinct MI210 --
"setting UNSLOTH_ROCM_GFX_ARCH will not enable ROCm PyTorch" was false:
masking to the other card and pinning its arch installs them, and install.sh
routes exactly that host to gfx110X-all a few lines earlier. Every advice site
now says what is true of the card it just named and claims nothing beyond it.

Deciding it at runtime was tried and dropped. Reading "an AMD adapter neither
table names" as a working peer misfires on the Vega-class iGPU (Raven through
Cezanne, Mendocino) that sits beside the dGPU on most Ryzen desktops and has no
ROCm torch path of its own, which would trade a correct dead stop for the
open-ended errand this change exists to remove. Reading only the supported
table misses the Instinct and V620 parts that are routable and appear in no
name table at all. Neither rule is right often enough to speak for a host.

Four real boards are added to all five copies of the message-only table:
Radeon Pro 5700 / 5700 XT (pci.ids 7319 and 731b, Navi 10, gfx1010) are the
only Navi 10 retail parts whose name carries neither "RX 5700" nor a W prefix,
and Radeon Pro WX 7100 / WX 5100 (Ellesmere, gfx803) carry no RX number at all,
so both fell through to the generic advice. Provenance from pci.ids as before,
and the shell case arms are matched case-sensitively, which is now stated where
only the arm ordering was.

Tests:

- The absolute host-wide phrasings are banned from all five sources, with the
  scoped replacements required per site so deleting the sentence cannot pass.
- test_unsupported_arch_routing_guards_8529.py drives the real index resolvers
  rather than asserting table shape: install.sh's get_torch_index_url and
  _amd_arch_index_family_for_gfx under sh, the .ps1 family maps under pwsh
  (including writes after the declaration, and the -contains list), the Python
  resolvers on both platforms, the Strix per-arch reroute, and studio/setup.sh's
  report-only lookup. Positive controls throughout.
- _assert_guarded_by_pin_arm matches the pin arm exactly rather than by prefix,
  binds the message's own enclosing arm, requires the pin arm to still say its
  note, ignores comment lines, and detects inline and re-indented chain closes.

Mutation tested: 13 mutants covering a post-declaration map write in each .ps1,
a single-quoted arch in _rocmWheelArches, a bypass inside get_torch_index_url,
the Strix arm, feeding the unsupported lookup into _setup_gfx directly and one
hop later, an inline-closed pin chain, a re-indented fi, and removing each new
table row. All killed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Say whose wheels are missing: AMD ships RDNA 1 PyTorch, Unsloth does not install it

"No ROCm PyTorch wheels exist for that arch" is no longer true of RDNA 1. AMD's
TheRock lists device-gfx1010, device-gfx1011 and device-gfx1012 as installable
torch extras on its multi-arch index, and SUPPORTED_GPUS.md marks all three
Build Passing, Sanity Tested and Release Ready. gfx803 is absent from that table
entirely, and no GCN4 family appears at all, so the Polaris half stands.

The claim these installers can honestly make is about their own routing, not
about ROCm at large: repo.amd.com publishes gfx103X/110X/1150/1151/120X and
nothing for gfx101X or gfx80X, so UNSLOTH_ROCM_GFX_ARCH=gfx1010 still lands on
the unmapped path and still returns CPU. Every site now says Unsloth has no
wheels for the arch rather than that none exist, and the tables carry a note
saying why the wording is scoped. Routing, the CPU fallback and the Vulkan
advice are all unchanged.

Verified by running the merge base and this branch side by side under identical
stubbed hardware, and diffing:

- POSIX shell, 127 simulated hosts x 2 blocks = 254 rows, each run in both
  trees. Selected torch index URL differed in 0 rows, exit code in 0 rows,
  get_torch_index_url stdout in 0 rows. The 32 rows whose end-of-run summary
  text moved are all AMD arch-unknown hosts resolving to gfx1010/1011/1012/803.
  Covers linux/wsl/macos/aarch64, NVIDIA at five CUDA levels, 15 supported
  gfx arches, multi-GPU lspci mixes, every override, dash and bash, set -eu,
  and lspci absent/failing/hanging. Asserted separately that the new WARN lines
  go to stderr, so TORCH_INDEX_URL=$(get_torch_index_url) is never polluted:
  0 of 127 index rows had anything but one URL on stdout.
- PowerShell, 232 cells over 116 adapter inventories x install.ps1 and
  setup.ps1. Resolved arch, index URL, arch family, routing flag and the
  gfx handoff are identical in every cell. 80 cells changed text, all RDNA 1
  or Polaris. An RDNA 1 card beside an RX 7900 keeps the old wording and still
  resolves gfx1100, which is the covered-peer guard doing its job. Under
  Set-StrictMode the new code passes only because of the variable
  initialisation added outside the detection block; removing it fails.
- Python stack, 23034 value comparisons over 5 platforms x 4 GPU classes x 38
  arch inputs x 99 adapter names x masks x overrides x 6 mirror configs.
  0 value differences. No unsupported arch produced an AMD index URL by any
  path in either tree, and no supported arch changed URL. A negative control
  that adds gfx1010 to the routing map reports 83 differences, so the zero is
  real.
- Existing installs: a manifest written by the old code verifies identically
  under the new code and vice versa, over 12 write/read combinations; a
  manifest poisoned with gfx_arch and index-url keys changes no verdict,
  because nothing arch-shaped is persisted. The legacy UNSLOTH_FORCE_VULKAN
  resolves identically across all 90 combinations with
  UNSLOTH_LLAMA_CPP_BACKEND, including falsey values and =hip overriding a
  stale truthy legacy value.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Declare the unsupported-arch variable where its readers can see it

$ROCmUnsupportedGfxArch was declared inside `if (-not $HasNvidiaSmi)` in
install.ps1, but the arms that read it sit outside that gate, so on an NVIDIA
host the read is of a variable that was never assigned. Its five neighbours
(HasROCm, HipSdkInstalled, ROCmGpuLabel, ROCmVersion, ROCmGfxArch) are all
declared above the gate, and studio/setup.ps1 already hoists its own copy for
exactly this reason, so this one was the odd one out.

Harmless as shipped, because Install-UnslothStudio runs with Set-StrictMode off.
Under a caller's `Set-StrictMode -Version Latest` it is a hard stop: driving the
extracted blocks under pwsh, an NVIDIA host that lands on the /cpu leaf (a
pre-CUDA-11 driver, or UNSLOTH_TORCH_INDEX_URL pinned to cpu) throws on the
read. Found by running this branch and its merge base side by side over 240
Windows cells; every resolved arch, index URL, arch family, routing flag and
gfx handoff matched, and this was the only asymmetry that was not message text.

A test pins the declaration above the gate in install.ps1 and at script scope in
setup.ps1. Moving it back inside the block fails the test.

* Let an identified uncovered card outrank the generic ROCm report, and teach export

Two review findings, both reproduced first.

amd-smi can report a GPU with no gfx token anywhere in `list` or `static --asic`
and only a market name. That sets $HasROCm with no arch, so the generic
`} elseif ($HasROCm)` arm fired and called an RX 5700 XT "AMD ROCm (AMD Radeon
RX 5700 XT)" while the wheel note in the same run said gfx1010 has none. Driving
the real detection and step chain under pwsh with a stubbed amd-smi reproduces
it exactly, and the host is not hypothetical: amd-smi is only probed when the
HIP SDK is present, which is what the #8529 and #8458 reporters installed
because the old message told them to. Both scripts now carry the same
`-and -not $ROCmUnsupportedGfxArch` guard the HIP SDK arm below already had.
A supported gfx1100 host and an unmapped Instinct MI210 host are unchanged on
the same harness, since the guard is a no-op when no arch was identified.

The POSIX advice said to `set UNSLOTH_LLAMA_CPP_BACKEND=vulkan` and re-run the
installer. A bare assignment is a shell variable, not an environment entry, so
the installer subprocess never sees it and the user gets the CPU bundle again:

    $ sh -c 'UNSLOTH_LLAMA_CPP_BACKEND=vulkan
      ./installer'      -> installer sees: [<unset>]
    $ sh -c 'export UNSLOTH_LLAMA_CPP_BACKEND=vulkan
      ./installer'      -> installer sees: [vulkan]

That is the #8458 failure mode reintroduced by the fix for it. The README block
has always used export; the three POSIX message sites now agree with it. The
PowerShell sites already used `$env:`, which is the process environment, so they
were correct and are untouched.

Tests pin both: the emitted POSIX setter is now `export ...`, and each generic
ROCm arm must carry the unsupported guard. Dropping either guard fails.

* Guard the ROCm summary chain the same way its two siblings are

The summary at studio/setup.ps1 opens with a bare `if ($HasROCm)` rather than
an `} elseif`, so it was missed when the other two chains were guarded. Its own
third arm names the uncovered card, and that arm is only reached when nothing
outranks it. On a host where amd-smi enumerates an RDNA 1 card with no gfx
token, the "ROCm x.y" arm wins and the arm written for that card never runs,
so the same run reports ROCm here and no wheels below.

The existing check only matched arms opening with `} elseif ($HasROCm`, which
is why it walked past this one. The new test finds the chain by its own body.

* Keep banning the bare POSIX setter in the PowerShell sources

Requiring `export` in the .sh advice was done by redefining _POSIX_SETTER, which
is also the needle two PowerShell bans read. With the export folded in, a .ps1 that
printed a bare UNSLOTH_LLAMA_CPP_BACKEND=vulkan no longer matched either ban: a
mutant adding exactly that line passed all 261 tests in the file. Split the two.
_POSIX_ASSIGNMENT is the bare form the Windows sources must never print, and
_POSIX_SETTER stays the exported form the POSIX ones must teach. The same mutant
now fails, and reverting either export still fails.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten the comments on the unsupported-arch path

Final pass over the comments this branch added. The five copies of the table
header each repeated the same four points at four different lengths, so they
are now one block of the same wording everywhere, and the provenance and
scoped-claim notes are folded into it rather than trailing it. Same for the
report arms and the test rationales: no reason removed, fewer lines to read.

Comments, docstrings and wrapping only. AST-checked with comment_tools.py
(4/4 code-unchanged), `bash -n` on both POSIX scripts, and the PowerShell
parser on all three .ps1 files. Suites re-run: 1036 passed, 1 skipped, and
the pwsh behavioural suite green.

* Blame the right card in the CPU summary, and stop promising macOS Vulkan

Two more review findings, both reproduced first.

The end-of-run CPU summary calls the lspci lookup unconditionally, so unlike
the arm in get_torch_index_url it is not covered by the empty-probe gate. On a
host pairing an RX 5700 with an RX 7900, a CPU fallback caused by the 7900's
ROCm being older than 6.0 was attributed to the 5700, replacing the "upgrade
ROCm" advice with advice that is false for the card that actually caused it.
Running the shipped guard under sh with a stubbed lspci, only that host moves:

    lone RX 5700       uncovered-card message   -> unchanged
    lone RX 580        uncovered-card message   -> unchanged
    lone RX 7900       generic message          -> unchanged
    lone MI210         generic message          -> unchanged
    RX 5700 + RX 7900  uncovered-card message   -> generic message

The summary now asks _infer_linux_amd_gfx_arch, which scans every display
adapter, and stays quiet when any of them is covered. Same shape as the peer
guard install.ps1 already carries.

The README's Vulkan paragraph sat under the combined "macOS, Linux, WSL"
heading. macOS has no Vulkan llama.cpp bundle: install_llama_prebuilt.py logs
that the variable is ignored and installs the Metal build, and upstream ships
no macOS Vulkan asset either (Metal is the default there, and Vulkan on macOS
only exists through MoltenVK, which you have to build yourself). An Intel Mac
carrying one of these very cards, the 16-inch MacBook Pro shipped the Radeon
Pro 5300M/5500M/5600M, would follow the command and get nothing. The paragraph
now names Linux and WSL and macOS gets its own sentence. The installer itself
needed no change: get_torch_index_url returns before any AMD probe on Darwin
and the summary is already gated on it, so the advice was never emitted there.

Both guards are pinned by tests that fail when the guard is dropped.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten the comments added in the last pass

* Guard the Studio report's peer scan, explicit index pins, and Windows ARM64

Three more review findings, all reproduced first.

studio/setup.sh had the same misattribution I fixed in install.sh last round,
on the KFD path where neither rocminfo nor amd-smi answers and the lookup falls
back to lspci. First match wins, so a host whose RX 5700 enumerates before an
RX 7900 was told no override could help, which is false there. The supported
name table is now a matcher, _setup_supported_gfx_from_name, with its arms
byte-identical to before, so the scan can ask about a peer without touching
$_setup_gfx. Same fixtures as the install.sh guard, run under sh:

    lone RX 5700 / RX 580        named          -> unchanged
    RX 5700 + RX 7900, either order   named     -> quiet
    RX 580 + RX 7900             named          -> quiet

An explicit UNSLOTH_TORCH_INDEX_URL or _FAMILY reaches the ROCm install path
for any gfx*/rocm* leaf, so "torch stays CPU-only and neither the HIP SDK nor
UNSLOTH_ROCM_GFX_ARCH changes that" was false on a pinned run. install.sh's CPU
note already skipped its guidance when pinned; install.ps1, studio/setup.ps1,
studio/setup.sh and install_python_stack.py now agree with it. install.ps1's
second site needed nothing, since it already sits behind -not $ROCmIndexUrl.

studio/setup.ps1 throws on UNSLOTH_LLAMA_CPP_BACKEND=vulkan on Windows ARM64,
where no Vulkan bundle is published, so the advice aborted the next update
instead of enabling GGUF acceleration. Both PowerShell sites now branch on
Get-HostMachineArch and point at a source build there.

The advice-window test had to change with them: the claims now sit in if/else
arms, and a fixed 8-line window either stopped mid-branch or spilled into the
next arm, which is the failure its own docstring warns about. It walks to the
end of the enclosing arm instead, capped. Both bash table-parity tests follow
the matcher's new variable names and still compare the same rows.

Each guard is pinned by a test that fails when it is dropped: the peer loop,
the pin check, and the Get-HostMachineArch call were each mutated and killed.

* Cut the comment lines that restated the message below them

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fill the Windows peer list on the amd-smi path, and three smaller corrections

Four more review findings, all reproduced first.

install.ps1's WMI scan sat behind `if (-not $HasROCm)`. amd-smi can report GPUs
with no gfx token and only the first market name, which sets $HasROCm with no
arch, so the scan was skipped and the peer guard added last round saw an empty
list on exactly the multi-GPU host it exists for. It is now keyed on the arch,
which is the condition under which the unsupported lookup can run at all: a
host that already has an arch still does no WMI work here, and amd-smi's label
still wins when it had one. The pwsh suite asserts the gate through the AST.

studio/setup.sh returned the failure of a nonempty market name immediately, so
a generic "AMD Radeon Graphics" from rocminfo ended the lookup before the lspci
scan and the report fell back to the plain "AMD ROCm" line this change is meant
to replace. It now returns only on a hit; a name that maps still short-circuits
without touching lspci, and the peer guard still covers the mixed host.

install_python_stack.py prints the same Vulkan advice as install.ps1 on the
Windows WMI path, and the same ARM64 throw applies to it. Added
_is_windows_arm64(), mirroring Get-HostMachineArch down to the
PROCESSOR_ARCHITEW6432 case an emulated x64 Python needs.

The setup.sh pin check treated a whitespace-only value as a pin, while
get_torch_index_url trims both variables and treats a blank one as unset. It is
trimmed the same way now. The variable also collided with the XPU block's
_setup_pin, which is a global in POSIX sh, so it is renamed _setup_unsup_pin.

One existing test asserted the behaviour the second finding calls a bug, that
an unrecognised reported name claims nothing. Rewritten to the corrected
intent, keeping a case that proves a covered card is still never claimed from
lspci. test_windows_amd_gpu_scan_fallback.py extracts the WMI block by its
literal gate, so its anchor follows the new condition.

Each guard is pinned by a test that fails when it is dropped: all four were
mutated and killed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Cover the Python emitter in the ARM64 Vulkan-offer guard

* Fill the Studio peer list on the amd-smi path too

studio/setup.ps1 carries the same WMI scan install.ps1 does, for its own name
inference, and it had the same gate. amd-smi can report GPUs with no gfx token
and only the first market name, which sets $HasROCm with no arch, so the scan
was skipped, $script:ROCmGpuLabels held one name, $gpuNames was one entry, and
a host pairing an RX 5700 with an RX 7900 was judged entirely on the 5700 even
with HIP_VISIBLE_DEVICES=1 selecting the 7900.

The scan is now gated on the arch, which is the condition the inference block
below already runs under, so the two can no longer disagree about whether there
is anything to infer from. A host that already has an arch still does no WMI
work here, and amd-smi's label still wins when it had one: only the peer list
is new on that path.

The pwsh suite walks the AST from the $script:ROCmGpuLabels assignment to its
enclosing if and asserts the condition names the arch and not $HasROCm, as it
already does for install.ps1. Restoring the old gate fails it.

tests/test_windows_amd_gpu_scan_fallback.py extracts that block with a regex
anchored on the literal gate, so its pattern follows the new condition.

* Run the covered-peer guard before the named hit too

The guard added last round only covered setup.sh's lspci fallback, so a named
hit still walked past it. amd-smi reports one market name, the first device's,
so on a host whose RX 5700 precedes an RX 7900 the name IS the uncovered card
and the false verdict came back through the other door.

The scan now runs first, for both paths:

    lone RX 5700, named            named    -> unchanged
    lone RX 580, named             named    -> unchanged
    RX 5700 + RX 7900, named 5700  named    -> quiet
    RX 5700 + RX 7900, no name     quiet    -> unchanged

It must not become a silencer, so it applies only when lspci can answer: with
no adapter list there is no peer to find, and the single-card host this report
exists for still has to be told. A test drives the lookup with lspci off PATH
and requires the verdict to survive.

Putting the named hit back in front fails the two mixed-host cases while that
no-lspci control still passes, so the ordering is what is pinned, not just the
presence of the guard.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten the comments added since the last pass

* Ask CIM for the adapter list, not the cmdlet PowerShell 7 dropped

install.ps1:3331 was the only live Get-WmiObject call left in the file; every
other WMI query in it already asks Get-CimInstance, and the script handles
PSEdition Core explicitly. Get-WmiObject was superseded by Get-CimInstance in
PowerShell 3.0 and removed outright in PowerShell 7, so on pwsh the call threw,
the block's own catch swallowed it, and $wmiAmdNames came back empty. That is
the peer list the guard added two rounds ago reads, so on pwsh the guard could
never fire and the amd-smi path went back to blaming the uncovered card.

Same class, same properties, and the CIM cmdlets ship with 5.1 as well, so the
swap costs nothing downlevel and matches what studio/setup.ps1 already does.

The harness stubbed Get-WmiObject, so it was answering for a cmdlet the
installer no longer calls. It now stubs Get-CimInstance and defines
Get-WmiObject to throw, so a revert fails loudly instead of quietly returning
nothing through that catch. Reverting the call fails four tests.

* Tighten the two comments from the last pass

---------

Co-authored-by: danielhanchen <danielhanchen@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <unslothshared@gmail.com>
2026-08-13 05:21:12 -07:00
oobabooga
5426a78c39
Studio: switch llama.cpp backends from the UI (#8520)
* Studio: add llama.cpp backend selector

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix llama.cpp backend selection edge cases

* Fix llama.cpp backend selection review issues

* Fix remaining llama.cpp backend review issues

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix llama.cpp backend switch consistency

* Re-pair whisper after llama runtime changes

* Re-pair whisper when llama runtime identity changes

* Unify llama backend selection invariants

* Preserve llama backend fallback constraints

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Publish llama planning job state

* Handle llama frontend job transitions

* Simplify the llama.cpp backend selection contracts

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address the open Codex findings on backend precedence and job ownership

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Drop the unused llama backend imports the hoist check rejects

The Source lint job fails on studio/install_llama_prebuilt.py: scripts/verify_import_hoist.py
reports INSTALL_KIND_BACKENDS and marker_backend as hoisted but unused.

marker_backend is genuinely dead here, so it goes. INSTALL_KIND_BACKENDS is not: the
installer is meant to share one vocabulary with the marker readers, and two tests assert
that through this module. Give it a real use instead of a bare re-export by deriving
VULKAN_INSTALL_KINDS from the map rather than spelling the same two names out again, which
also removes a mirrored definition of the kind this file warns about elsewhere.

* Treat an install marker that is not a JSON object as no marker

read_install_marker returns whatever json.loads produced, so a marker holding [] or 123
reaches callers as something without .get. Every caller assumes a mapping, and the new
backend picker adds one more: get_backend_status calls marker_backend(marker) and raises
AttributeError, so GET /api/llama/backend answers 500 and Settings > System shows a load
error for what is only a corrupt file.

This is not new on this branch (get_update_status raises the same way on main), but the
picker makes it reachable from a page users open. Guard it where the file is read, so the
update planner, the picker and crash recovery all degrade to the source-build path
together, exactly as they already do for unparseable JSON.

* Disable Apply when the environment pins the backend

The Select is disabled whenever env_backend is set, but Apply is gated only on dirtiness,
and the two are computed independently. An automatic install whose detection has since
drifted reports selection_applied false, so the row is dirty while the Select is disabled
and Apply becomes the only live control. POST /api/llama/backend then refuses it with
environment_override, which is correct, but the button should not have offered it.

Also fills in the status shapes the payload tests did not reach: every unsupported reason,
a macOS install reporting metal, and the terminal job states.

* Run the setup.ps1 exit routing under pwsh instead of matching its text

The Windows half of the fail-closed change was asserted by comparing source strings, which
cannot catch a branch that reads the same and behaves differently. Extract the routing
block the way the bash harness already does, run it under pwsh, and require the same
decision from both: identical exit code, and identical answer on whether a source build
was queued.

Covers exit codes 0/1/2/3/4/5/137 against each explicit backend and both install states,
70 pairs, and pins the two branches the picker depends on: exit 5 fails closed everywhere,
exit 2 stays the one automatic path allowed to fall back to a compile. Skipped where pwsh
is absent, like the other PowerShell tests here.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Let a failed whisper re-pair be retried from the same selection

The llama phase runs first and records the new backend, so a retryable whisper failure
(a dropped download, an install that was busy) ends with llama.cpp on the requested
backend and dictation still hardlinked to the old runtime. Retrying that selection is
then already_selected, which skips the llama phase, and the whisper planner refused to
run without one. The reported failure was unfixable except by switching away and back.

Allow a repair-only job for that one refusal, gated on the pairing actually being stale
so an ordinary already-selected request stays a refusal instead of becoming a no-op job
that reports success. slim_pairing_is_stale exposes the comparison run_repair_phase
already makes before it does any work.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
2026-08-13 02:33:20 -07:00
Lucas
51fac012be
Fix Cloudflare documentation link (#8572) 2026-08-12 11:38:49 -03:00
Michael Han
4caf91c87e
Update README.md 2026-08-12 05:56:19 -07:00
Michael Han
4acc36b107
Update README.md
Some checks are pending
Backend CI / Repo tests (CPU) (push) Waiting to run
Unsloth export capability / capability (ubuntu-latest) (push) Waiting to run
Unsloth export capability / capability (windows-latest) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Unsloth GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Unsloth GGUF CI / Tool calling Tests (push) Waiting to run
Unsloth GGUF CI / JSON, images (push) Waiting to run
Unsloth load-orchestrator CI / test (push) Waiting to run
Mac Studio GGUF CI / GGUF inference smoke (API, tools, vision) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio UI + API + Update CI / Chat UI, API and Update Tests (push) Waiting to run
Unsloth Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Unsloth Tauri CI / Rust unit tests (windows) (push) Waiting to run
Unsloth UI CI / Chat UI Tests (push) Waiting to run
Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Windows Unsloth API CI / Unsloth API & Auth Tests (push) Waiting to run
Windows Unsloth GGUF CI / GGUF inference smoke (API, tools, vision) (push) Waiting to run
Windows Unsloth GGUF CI / Unsloth install + inference without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Windows Unsloth UI CI / Chat UI Tests (push) Waiting to run
Windows Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
2026-08-11 14:21:48 -07:00
Daniel Han
8fd61b5f05 Update README.md 2026-08-11 13:19:31 -07:00
Daniel Han
49164812ad Update README.md 2026-08-11 13:19:01 -07:00
Daniel Han
d96eef50b9 Update README.md 2026-08-11 13:10:48 -07:00
Daniel Han
c80a3d1039 Update README.md 2026-08-11 08:58:31 -07:00
Daniel Han
8a237be645 Update README.md 2026-08-11 07:53:11 -07:00
Daniel Han
f8b8bb720f Update README.md 2026-08-11 07:38:30 -07:00
Daniel Han
798006882a Update README.md 2026-08-10 07:32:22 -07:00
Michael Han
c52e035801
Update README.md 2026-08-10 05:31:42 -07:00
Daniel Han
c2a91dbe6d
Revert "Add option to launch Studio without opening the default browser (#7016)" (#8040)
This reverts commit 3860d69ab2.
2026-08-06 08:39:06 -07:00
Michael Han
3860d69ab2
Add option to launch Studio without opening the default browser (#7016)
* Add no-browser launch option for the Studio desktop launcher

The generated launchers (launch-studio.sh / launch-studio.ps1) always
opened the default browser once the server became healthy. Add a
--no-browser launcher flag, the UNSLOTH_STUDIO_NO_BROWSER env var, and
a persisted installer preference (studio.conf / baked into the ps1
launcher) with an interactive install prompt. When auto-open is off the
launcher still starts or attaches to the server and prints the URL, for
users who run Studio as a browser PWA or app window. The
--shortcuts-only refresh run by studio update preserves the choice.

* Make UNSLOTH_STUDIO_NO_BROWSER falsy check case-insensitive in the shell launcher

Simulation testing caught that False or Off disabled the browser in
launch-studio.sh while the PowerShell launcher's -notin treats them as
falsy case-insensitively. Lowercase the value before matching so both
launchers agree, and pin the behavior in the launcher test.

* Run the launcher no-browser shell test in CI

The shell installer test step runs a hardcoded list, so the new
tests/sh/test_launcher_no_browser.sh was only bash -n parsed by lint
and never executed. Add it to the list; it only reads install.sh and
install.ps1 and writes to mktemp sandboxes, so it fits the step's
no-writable-tree constraint.

* Keep the saved browser preference when the reinstall prompt is accepted

An interactive reinstall over an install that had persisted
STUDIO_OPEN_BROWSER='0' would flip it back to 1 when the user pressed
Enter, because the prompt default was hardcoded to yes and the answer
then overrode the preserve logic. Seed the prompt default from the
existing preference (studio.conf on macOS/Linux/WSL, the value baked in
launch-studio.ps1 on Windows) and flip the hint to [y/N] accordingly.
Explicit y/n answers still override.

* Silence SIGPIPE noise in the launcher no-browser test

grep -q exiting on first match SIGPIPEs the echo feeding it when the
haystack is the whole installer, spamming 'write error: Broken pipe'
in the CI job log. Feed grep from here-strings instead.

* Address review feedback: reroute flags, curl EPIPE, post-install browser open

Three fixes from PR review and field testing:

- Forward an explicit --no-browser/--browser choice into the WSL Strix
  Halo reroute so the rerouted install honors the flag.
- Drain piped stdin before the --shortcuts-only early exit so
  curl | sh -s -- --shortcuts-only no longer dies with curl error 23.
- Open the browser after the installer's own foreground launch when the
  preference is on: a background watcher polls /api/health, verifies
  the per-install studio_root_id so a different Studio on the port is
  never opened, then opens the URL once. Mirrored in install.ps1 with a
  Start-Job watcher. When the preference is off nothing changes; the
  server already prints its URL.

* Use selected port for post-install browser watcher

* Gate the browser prompt on skip-autostart, stub the WSL openers in its test

Addresses the two open review items, plus one leak found while confirming them.

UNSLOTH_SKIP_AUTOSTART is documented for automated installs, but the new browser
prompt did not check it: under `curl ... | sh` only stdin is the pipe, so [ -t 1 ]
is still true and the prompt blocked an install that previously ran unattended.
Both installers now gate it exactly as they gate the launch prompt below it.
Reproduced under a real pty before and after, and pinned by two assertions that
are absent at the previous head.

tests/sh/test_launcher_no_browser.sh stubbed only open and xdg-open, but
_open_browser prefers powershell.exe then cmd.exe on WSL, so running the suite on
a WSL machine opened a real browser window and then failed on the empty recorder.
All four openers are stubbed, PATH is pinned to them plus coreutils, and
/proc/version is a fixture, so one Linux runner now covers every rung of the WSL
ladder and the no-opener fallback.

The post-install watcher also outlived the installer: its pid was never kept, so
Ctrl-C on the foreground server left it polling out its 120s deadline and it
could open a browser after the install had visibly finished. It is now reaped
once the server exits, and the PowerShell job is stopped and removed in a
finally, matching what install.ps1 already does for its other Start-Job. The
watcher no longer treats a missing studio_install_id as "any backend will do".

---------

Co-authored-by: Unsloth <michaelhan@Michaels-MacBook-Pro.local>
2026-08-06 04:51:05 -07:00
Michael Han
fbf599a289
Studio: settings UI cleanup for Agents, System and Connections (#7594)
* Studio: tidy up the Agents, System and Connections settings

Agents tab
- Coding agent, model and quantization now sit on one row of three
  columns instead of stacking.
- Smaller agent logos so the picker reads as a list, not a row of tiles.
- Command boxes lose their outer border and carry the copy control
  inside the box, top right.
- The `unsloth start` chip is now the link to the docs.
- Quantization dropdown drops the extra tags that wrapped every row onto
  two lines, and the size sits flush right so the list scans cleanly.
- More breathing room between sections.

System tab
- Rounder metric cards with more padding, and no resting border in dark
  mode.
- Darker meter track in dark mode so the bars stand out again.
- GPU devices collapse to one row per device; a full width bar under a
  single GPU read like a page-wide rule.

Connections
- The add-provider box matches the corner radius used elsewhere.

Appearance
- Colour palette cards drop the resting border in dark mode, matching
  the rest of Settings. Hover and selected states are unchanged.

* Studio: keep per-GPU memory figures on narrow screens

The one-row device layout hid used, free and total below 1024px, so
tablets and small laptops were left with only the utilization
percentage. Those widths now stack the row instead of dropping the
figures, and the bar goes full width under them.

* Studio: let the GPU meter shrink beside its percentage

The stacked layout gave the meter the full row width while keeping it
shrink-0, so each device row ran wider than its container once the
figures moved onto their own line. It now fills whatever the percentage
label leaves, and keeps the fixed width at lg and above.

* Studio: size the GPU row to its pane and fix the docs link name

- The device row keyed its layout off the viewport, but the Settings
  dialog caps the tab at roughly 650px no matter how wide the window is,
  so on every desktop the row stayed horizontal and squeezed the device
  name down to a fragment. It now uses a container query against the
  pane and only goes to one row when there is genuinely space.
- The docs link carried an aria-label that replaced its visible
  "unsloth start" text as the accessible name, so it could not be
  targeted by the label users can see. The title keeps the description.
- Dropped a stale agent from the swap hint so it matches the picker.

* Studio: size Agents and System to their pane, restore palette focus

- The GPU row's breakpoint could never fire. The settings pane is about
  664px at any window size, and the name, three figures, meter and
  percentage need more than that, so the one-row layout was dead code.
  Each device is now two compact lines: name with the percentage, then
  the figures with a fixed-width meter. The name stays readable, the
  meter no longer runs the width of the pane, and there is no breakpoint
  that cannot be reached.
- The agent, model and quantization grid switched on the viewport, so
  between 768px and 1024px it went to three columns inside a 440px pane
  and crushed every control. It now keys on the pane and stacks until
  the columns genuinely fit.
- Dark mode dropped the palette card's resting border, but that border
  is the only focus indicator since the card removes its outline, and an
  unlayered rule beats Tailwind's layered focus-visible utility. Keyboard
  focus paints the ring colour again.

* Studio: align the localized agent swap hints with the picker

The English hint was updated to match the agents the picker offers, but
the ten translated copies were not, so every non-English user was told to
pick an option the UI does not show.

* Studio: rework the GPU device row

Restores the earlier shape, name over backend on the left, but lifts the
memory figures and the meter up beside them instead of stacking them
underneath. The meter is short rather than full width, which was the
original complaint: at full width it read as a rule across the pane
rather than a reading for one device.

VRAM utilization becomes a rounded pill next to the backend label. The
row wraps rather than squeezing the device name when the pane is narrow.

* Studio: align the three Agents column headers

The Coding agent header carries a padded docs link, so it measured
taller than the plain labels beside it and pushed its select down.
Pin every column header to the same height.

* Studio: stack the GPU meter under its figures and tint the VRAM pill

The meter sat beside the used/free/total figures with only a fixed 24
width, so it read as squeezed. Move it under them where it spans their
width. The pill was muted grey and washed out in light mode; give it the
same accent fill the New tags use.

* Studio: rule the GPU figures apart and drop trailing zeros

Used, free and total ran together with only a gap between them, so the
three readings were easy to misread as one. Rule them apart, size the
numbers down a step, and widen the block so it starts further left.
Whole values now read as 64 GiB rather than 64.0 GiB.

* Studio: widen the GPU figures to the Model downloads edge

Match the 392px of the Model downloads control so both blocks start on
the same edge, and open up the gap between the figures and the meter.
Below 992px the dialog no longer fills its width cap, so stack the row
rather than squeezing the device name.

* Studio: let the GPU readings truncate instead of overflowing

The three figures carry truncate but sit in a flex row, where the
default min-width:auto stops an item shrinking below its text. The
longest locales would push past the block rather than ellipsize.

---------

Co-authored-by: Unsloth <michaelhan@Michaels-MacBook-Pro.local>
2026-07-29 07:37:27 -07:00
Leo Borcherding
cd5011f288
Studio: add UNSLOTH_STUDIO_DISABLE_PUBLIC_CHECK to switch off the startup public lookups (#7433)
Some checks are pending
Unsloth GGUF CI / JSON, images (push) Waiting to run
Unsloth load-orchestrator CI / test (push) Waiting to run
Mac Studio API CI / Unsloth API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Unsloth Updating Tests (push) Waiting to run
Unsloth Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Unsloth UI CI / Chat UI Tests (push) Waiting to run
Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Windows Unsloth API CI / Unsloth API & Auth Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Windows Unsloth GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Unsloth GGUF CI / Tool calling Tests (push) Waiting to run
Windows Unsloth GGUF CI / JSON, images (push) Waiting to run
Windows Unsloth GGUF CI / Unsloth install + inference without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Windows Unsloth UI CI / Chat UI Tests (push) Waiting to run
Windows Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
* Studio: make the startup public-IP lookup opt-in (#7307 P8)

Startup resolved the machine's external IP by asking ifconfig.me whenever
Studio bound to 0.0.0.0 or ::. That tells whoever runs that service this
host is running Unsloth, which the user never agreed to.

The lookup is now gated behind UNSLOTH_STUDIO_PUBLIC_IP_PROBE, off by
default, and logs plainly what it sends and where when enabled. Only
1/true/yes/on enable it, so a typo leaves the private default in place.

The other two steps stay unconditional because neither discloses
anything: the GCE metadata server is link-local, and the UDP connect only
asks the kernel which local address routes to 8.8.8.8 without putting a
packet on the wire. Disabling the probe therefore still yields a usable
LAN address for the access banner.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: gate the check-host.net reachability probe too, and keep the cloud address

Problem 8 of #7307 is the check-host.net probe in _verify_global_reachability: it hands this machine's address and port to a third party and asks its nodes to connect back. That was still unconditional, so on GCE and on any host whose routing address is already public the reported behaviour was unchanged. It is now behind the same UNSLOTH_STUDIO_PUBLIC_IP_PROBE opt-in.

A private interface address is not proof the port is unreachable, since a NAT or cloud firewall can forward it. The private-address path no longer sets _public_reachable = False, so the banner keeps its warning instead of claiming local network only.

To stop the privacy default from costing cloud users their shareable address, step 1 now reads AWS IMDSv2 and Azure IMDS alongside GCE on link-local 169.254.169.254.

Also drops the redundant function-local import os and documents the variable in the README remote access section.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: switch to a single UNSLOTH_STUDIO_DISABLE_PUBLIC_CHECK opt-out

Replaces the opt-in variable and the cloud metadata work from the previous commit. Both third-party startup lookups stay on by default, so nothing changes for existing users, and one variable turns both off for lab and privacy-sensitive deployments. That is the fallback the reporter offered in #7307 Problem 8, and it keeps the firewall diagnostic that the reachability check exists to provide.

The ifconfig.me lookup and the check-host.net probe are now both guarded by public_check_disabled(). Parsing matches the nearest existing switch, _trust_forwarded_for in utils/client_ip.py. The reachability guard sits after the private-address branch, which makes no network call, so a LAN user who opts out still gets the address note.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
2026-07-27 00:16:53 -07:00
alkinun
217e8f036c
fix(studio): report Vulkan GPUs in system UI (#7476)
* fix(studio): report Vulkan GPUs in system UI

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(studio): separate Vulkan inference GPU reporting

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(studio): keep retrying Vulkan probe refreshes

* fix(studio): preserve known zero GPU budgets

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-26 23:28:39 -07:00
Daniel Han
968e6230a0
Unsloth start: add local subagents for Claude Code, Codex, OpenCode and Pi (#7326)
Bring the local-subagent support onto main. The original change (#7316) merged
into the stacked pr/daniel-unsloth-start-audit branch rather than main, and #7313
reached main via squash, so these files never landed on main.

Adds --as-subagent for claude, codex, opencode and pi: the parent agent keeps its
own cloud model while a locally served GGUF is registered as a delegated subagent,
using ephemeral per-session config that never touches the user's real agent config.
2026-07-22 04:34:58 -07:00
Lee Jackson
39a999c056
Update README with latest features and Unsloth Start (#7258)
* Document local agent connections

* Refresh README features and news

* Tighten README feature copy

* Restore selective README emphasis

* Add Unsloth Start quickstart

* Update

* Mention

* Update README.md

* Reduce

* Restore-inference-order

* Split-agent-API-features
2026-07-20 05:57:14 -07:00
Michael Han
6d8c18cd1a
Replace standalone Studio wording with Unsloth (#7221)
* Replace standalone Studio wording with Unsloth

Replace the single word Studio with Unsloth wherever it is used as
shorthand for Unsloth Studio in docs, CLI output, UI strings, i18n
locales, workflow display names, comments and docstrings.

Kept unchanged: the full name Unsloth Studio, third party product
names (LM Studio, Visual Studio, Mac Studio), feature names
(Recipe Studio, Fine-tuning Studio and its translations), and all
identifiers such as env vars, commands, paths and filenames.

* Address review feedback on the Studio wording rename

Use "an" before Unsloth where the rename left the article as "a".
Restore the split brand where Unsloth and Studio render as two halves
of the full product name: the onboarding sidebar subtitle and the
IPv6 localhost warning. Scope two messages to the full name Unsloth
Studio where plain Unsloth was misleading: the AMD README bullet and
the CLI studio setup error.
2026-07-19 00:47:04 -07:00
Leo Borcherding
91a0df9514
Studio: make the Cloudflare tunnel opt-in (off by default) (#7046)
* Studio: make the Cloudflare tunnel opt-in (off by default)

A wildcard bind (`-H 0.0.0.0`) auto-started a public trycloudflare.com
tunnel, so exposing Studio on the LAN also published it to the public
internet. Flip the default so the tunnel is opt-in.

- `--cloudflare` is now tri-state (Optional[bool], default None = off),
  mirroring the existing --enable-tools/--disable-tools handling. Pass
  --cloudflare to expose a public HTTPS link for a wildcard bind; --secure
  still implies the tunnel.
- --secure + --no-cloudflare is still rejected as a contradiction.
- Update the parent-command guard, re-exec forwarding, startup-banner
  wording, the colab comment, README, and tests.

* Studio: update installer/setup launch hints for opt-in Cloudflare

The post-install launch hints only mentioned --secure for a public link.
Now that the tunnel is opt-in, clarify that -H 0.0.0.0 exposes the raw
port on the LAN (not a public URL), and surface --cloudflare as the
explicit opt-in for a public HTTPS link (--secure keeps the raw port
private). Applied to install.ps1, install.sh, and studio/setup.sh.

* Studio: address review - keep cloudflare tri-state + harden run re-exec

Two review points from the bots:

- Gemini: keep `cloudflare` as Optional[bool] in run_server instead of
  casting None -> False, so the startup banner can distinguish "OFF (default)"
  (unset) from "OFF (--no-cloudflare)" (explicit). `_cloudflare_flag` and the
  banner branch now carry the tri-state.
- Codex (P1): `unsloth studio run` re-execs the studio venv's console script,
  which can be an older build whose --cloudflare defaulted on; omitting the
  flag let it re-enable the tunnel. That path now forwards the default polarity
  explicitly (--no-cloudflare, or nothing under --secure since --secure implies
  the tunnel). The plain `unsloth studio` path runs the same-version in-tree
  run.py (resolved via _find_run_py), so it keeps forwarding only an explicit
  polarity and still shows the accurate "(default)" banner.

Tests updated for the tri-state banner labels, the None gate cases, and the
new re-exec forwarding.

* Studio: forward --no-cloudflare on plain re-exec too (mixed install)

Codex follow-up: _find_run_py falls back to STUDIO_HOME/.../studio/backend/
run.py when the package copy is absent, so the plain `unsloth studio` re-exec
can land on an older run.py whose --cloudflare defaults on. Forward the default
polarity explicitly there too (--no-cloudflare, or nothing under --secure),
matching the run subcommand. The common in-venv launch skips the re-exec and
still shows the tri-state "(default)" banner.

* Studio: fix launch hint - --cloudflare needs the wildcard bind

Codex P3: the launch hint listed --cloudflare next to the loopback
`unsloth studio -p 8888` command, but the tunnel only starts for wildcard
binds, so `--cloudflare` alone on 127.0.0.1 does nothing. Show
`-H 0.0.0.0 --cloudflare` in the hints (install.ps1, install.sh,
studio/setup.sh) and clarify the same in the README.

* Studio: cross-platform masked terminal password prompt helper

Per-keystroke '*' echo (POSIX termios cbreak / Windows msvcrt.getwch),
backspace editing, Ctrl-C abort, EOF handling, confirmation loop with
re-prompt on mismatch or policy failure. Pure should_prompt gate for the
--secure/--cloudflare exposure paths.

* Studio CLI: force a terminal password change before public tunnel exposure

When a launch will start the Cloudflare tunnel (--secure, or --cloudflare on
a non-api-only wildcard bind) and the admin account still has its seeded
bootstrap password, prompt for a new password in the terminal (masked with
'*', confirmed, re-prompting until valid) before any re-exec or server
exists. The change is committed in the parent so it never crosses argv or
the environment and older studio-venv children see it immediately. Without
a terminal, warn and fall back to the backend bootstrap shutdown timer.
Mirrors backend update_password semantics in one transaction: rehash,
rotate the JWT secret, clear must_change_password, revoke refresh tokens,
drop the desktop secret, then remove the stale credential files.

* Studio: terminal password gate before the public tunnel (backend backstop)

Never publish a trycloudflare URL while the seeded admin password is
active: run_server now runs a terminal password-change gate after the
tunnel decision and strictly before start_studio_tunnel. Interactive
refusal fails closed (shutdown + exit 1, mirroring the secure gate);
without a tty it warns and keeps the bootstrap deadline. Success applies
the same effects as the change-password route (update_password +
revoke_user_refresh_tokens) and drops the stale
app.state.bootstrap_password. MIN_PASSWORD_LENGTH centralised in
auth/storage.py and referenced by the HTTP schema. terminal_prompt.py
carries the pure gate helper (interactive loop stubbed; supplied by the
masked-input module). Also migrates the studio/setup.ps1 launch footer
that still showed the bare wildcard hint.

* README: reconcile remote-access section with opt-in Cloudflare tunnel

* Studio: harden the terminal password gate after review

- run.py: run the gate BEFORE the uvicorn socket binds. On a wildcard
  --cloudflare launch the served HTML injects the bootstrap credential
  for first login, so a pre-gate listener would hand the default
  password to anyone who reaches the raw port while the operator is
  still typing. The gate now also seeds the admin row itself (it can
  run before lifespan startup).
- Headless launches that nothing would protect now fail closed: the
  bootstrap deadline never arms for api-only serving and
  UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT=0 disables it, so warn-and-proceed
  would have promised a shutdown that never comes. Both the CLI and the
  backend refuse to publish in that case; the ordinary headless path
  still warns and relies on the 1h deadline, and no longer auto-fills
  the default credential into HTML served on a public URL.
- storage.update_password gains revoke_refresh_tokens to delete the
  user's refresh tokens in the SAME transaction as the password commit;
  the change-password route and the backend gate use it (a separable
  follow-up delete could fail after the commit and leave a stale
  refresh token able to mint access tokens under the rotated secret).
- clear_bootstrap_password is best-effort: a locked/undeletable file
  must not surface as a failed password change.
- CLI masked reader: disable ISIG like the backend so Ctrl-Z cannot
  suspend the process with the shared terminal stuck in no-echo mode;
  handle Ctrl-C/Ctrl-Z as characters; treat stream EOF mid-line as an
  abort instead of submitting a partial password. Both readers restore
  terminal attrs from a SIGTERM/SIGHUP handler since a finally block
  cannot run when a default-disposition signal terminates the process.
- Backend reader: decode byte-at-a-time through an incremental UTF-8
  decoder so multi-byte characters split across read boundaries are no
  longer dropped; isatty checks tolerate closed/None streams.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: persist bootstrap suppression through lifespan startup

The pre-bind password gate nulled app.state.bootstrap_password, but the
FastAPI lifespan runs after it and re-reads the bootstrap password into
app.state on both admin paths, so a headless public launch could still
serve the injected credential in HTML. Carry a persistent
suppress_bootstrap_injection flag that the lifespan honors instead.

Also drop the quoted Tuple annotation on _terminal_password_gate that
tripped the import-hoist lint (the typing import looked unused).

* Studio CLI: keep the pre-exec auth DB private (0700 dir, 0600 db)

On a fresh install the pre-exposure password gate creates auth/ and
auth.db through the CLI before the backend ever runs, and
sqlite3.connect leaves the DB 0644 under a 022 umask. Mirror backend
storage.get_connection's chmod so the committed password hash and JWT
secret are never world-readable, even if the launch aborts before the
backend applies its own modes.

* Tighten pre-exposure password gate comments

* Studio: delete seeded bootstrap password before headless public re-exec

The headless warn-and-proceed path returns with the default admin
password still active, then re-execs a child Studio process. An old
studio-venv child (mixed-version install) predates the pre-bind gate and
its injection-suppress flag, so its lifespan reads .bootstrap_password
and injects the seeded credential into the public HTML for up to the
bootstrap deadline. A CLI-flag handshake cannot fix this uniformly: the
studio run path uses ignore_unknown_options and an old in-venv child
runs in-process, so it would never reject the flag.

Delete the seeded .bootstrap_password file in the parent before re-exec
so a fresh child of any version reads None and never serves it. This
covers both re-exec paths and both child versions. must_change_password
stays set, so the login page still forces a change and the bootstrap
shutdown timer still arms; only the plaintext-on-disk copy is removed.
Recovery is via a terminal-attached run or reset-password. Backend gate
and CLI warnings updated to match.

* Studio: commit the seeded admin before headless public re-exec

The headless-warn path deletes the seeded .bootstrap_password so a
re-exec'd child cannot inject it, but _ensure_cli_default_admin's INSERT
was never committed and rolled back on conn.close(). On a fresh
STUDIO_HOME an old studio-venv child then found no admin, regenerated a
fresh bootstrap password + file, and injected THAT into the public page,
defeating the deletion.

Commit the seeded admin right after _ensure_cli_default_admin so any
re-exec'd child sees the existing account and does not regenerate.
Regression tests cover both re-exec paths on a fresh (unseeded) DB.

* Studio: fail closed when the bootstrap password file cannot be removed

On the headless public path, deleting .bootstrap_password is the
protection against an old re-exec'd child injecting the seeded
credential. If unlink fails (locked file, read-only auth dir) the file
is still on disk, so warning and proceeding would still leak it for the
bootstrap-timeout window. Abort with a clear error instead. Regression
test covers the unlink-failure fail-closed path.

* Studio: hold no-echo for the whole password line, not per keystroke

The POSIX masked reader set cbreak/no-echo inside _getch_posix and restored
the terminal to echo-on in a finally after every single keystroke, because
_read_password calls _getch once per character. Between one char returning and
the next call re-entering cbreak, ECHO was on, so a keystroke arriving in that
window echoed the password in cleartext.

Move the terminal mode into a _prompt_raw_mode context that _read_password
holds around the entire line (mirroring unsloth_cli/commands/_password_prompt.py,
which already did this), restoring once when the line completes or aborts.
_getch_posix now only reads, since the mode is held by the caller. The context
is a no-op when stdin is not a real terminal, keeping the _getch test seam.

Add a regression test asserting the raw-mode context wraps the read exactly
once and every keystroke is read while it is active.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: strip the seeded bootstrap password when the auth DB check fails

The pre-exposure gate returned early on two auth-DB inspection failures and
proceeded to re-exec without removing the seeded .bootstrap_password:

- _connect_auth_db() failure: a seeded credential from a prior run may still
  be on disk.
- the must_change_password read-back failure: worse, _ensure_cli_default_admin
  had already seeded the admin and the code committed it (writing
  .bootstrap_password) right before the failing SELECT.

In the mixed-version case (a new outer CLI re-execing an old studio-venv child
that predates the pre-bind gate), that child would read the file back and
inject the default admin credential into the public Cloudflare page. The
sibling headless branch already deletes the file for exactly this reason, so
these returns were an inconsistent gap.

Factor the delete-or-fail-closed logic into
_strip_seeded_bootstrap_password_or_exit and call it on both inspection
failures (and reuse it in the headless branch): strip the seeded file first
(version-independent protection), failing closed if the removal itself fails.
must_change_password stays set, so the login page still forces a change and the
bootstrap shutdown timer still arms.

Add tests for both new paths (connect failure and post-commit read-back
failure strip the file and proceed; a failed strip fails closed).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: fail closed when the seeded admin cannot be committed before exposure

The pre-exposure gate wrapped _ensure_cli_default_admin (the INSERT), its
conn.commit(), and the must_change_password read-back in one try, and the
except recovered by stripping .bootstrap_password and proceeding to re-exec on
the assumption the admin was already committed. That assumption only holds when
the failing statement is the SELECT. When the INSERT or the commit itself fails
(e.g. a write lock held past the busy timeout on a fresh install), no admin row
is committed: it rolls back on conn.close(), and a re-exec'd old studio-venv
child (no pre-bind gate) then finds no admin, regenerates a fresh bootstrap
password + file, and serves that default credential on the public Cloudflare
page. Stripping the file cannot stop a regeneration.

Split the seed+commit into its own try that fails closed (refuse the public
launch, best-effort removing any half-written seed file) since we cannot prove a
committed admin; keep the separate read-back failure on the strip-and-proceed
path, where the admin is committed so an old child finds it and will not
regenerate. Add a test for the seed-commit-failure path.

* Studio: decode the CLI masked password reader with errors="replace"

The CLI reader read keystrokes with text-mode sys.stdin.read(1), which raises
UnicodeDecodeError on a pasted non-UTF-8 password (e.g. Latin-1 bytes), or under
PYTHONUTF8 yields a lone surrogate that later crashes the pbkdf2 encode -- either
aborts the launch with a traceback. The backend mirror (terminal_prompt.py)
already reads raw bytes through an incremental decoder with errors="replace".
Mirror that here: read with os.read and an incremental decoder so invalid bytes
map to U+FFFD, iterating over each emitted char (one byte can complete a
replacement plus the next char).

* Studio: resolve the child launcher before the pre-exposure gate

The gate strips the seeded .bootstrap_password on a headless public launch, and
it ran before the re-exec launchability check (studio venv / run.py / console
script present). So a headless launch with an incomplete studio setup would seed
the admin, delete the bootstrap password, then abort because the child could not
be found, leaving the admin at must_change_password=1 with no password ever
shown or injectable: locked out until `unsloth studio reset-password`.

Resolve and validate the child launcher first, in both `studio` (studio_default)
and `studio run`, and only then run the gate, so an unlaunchable setup exits
before anything is stripped. Add a regression test that a missing venv exits
without removing the seeded file.

* Studio: fail closed when the auth DB cannot be opened before exposure

The connect-failure branch of the pre-exposure gate stripped .bootstrap_password
and proceeded, on the assumption a committed admin from a prior run made an old
child find it and not regenerate. But on a fresh public launch whose
_connect_auth_db() itself fails (transient lock during the schema/seed step, or
an unwritable home), no admin is committed, so a mixed-version re-exec child that
predates the backend gate can find no user, generate a fresh bootstrap password,
and serve it on the public Cloudflare page. Stripping a file we cannot vouch for
cannot stop a regeneration.

Make this branch fail closed like the seed/commit failure path: we only continue
past the DB inspection once a committed admin is confirmed. The existing file is
left untouched so a retry (after a transient lock clears) can still prompt.

Update the connect-failure test to assert fail-closed, and give the in-venv
--secure flag test a real STUDIO_HOME with an already-changed admin so the gate
is a no-op rather than relying on a DB-open failure.

* Studio: invalidate seeded bootstrap files before deleting auth.db on reset

reset-password deleted auth.db first, then best-effort unlinked the seeded
.bootstrap_password and desktop secret. unlink() only ignores
FileNotFoundError, so a locked or read-only file (Windows AV, read-only auth
dir) survived while auth.db was gone. The next server start then re-seeded
from that stale plaintext and re-validated the exact credential the reset was
meant to revoke.

Invalidate the credential files first, truncating any that cannot be
unlinked, then delete the DB, so a surviving file can never carry a reusable
secret. clear_bootstrap_password now truncates on unlink failure for the same
reason, and its warning says the contents were cleared rather than claiming
the stale password is already invalid.

* Studio: require a servable frontend before the pre-exposure gate can strip the seeded password

A headless public launch strips the seeded .bootstrap_password before the
re-exec'd child starts. If the child then cannot serve the login page (the only
in-band way to change the seeded password) the admin is locked out
(must_change_password=1, no file, no UI) until reset-password.

Add _require_servable_frontend_or_exit and call it before the gate on both
`unsloth studio` and `unsloth studio run` public launches: fail closed if a
non-api-only public launch has no built frontend dist, before anything is
stripped. A user-supplied --frontend is validated to contain index.html so a
bad path cannot silently bypass the check; an auto-resolved dist is trusted
(_find_frontend_dist already requires index.html) and forwarded to the child.

Model-load aborts on `studio run` remain a residual: the parent must strip for
mixed-version safety (an old studio-venv child has no pre-bind gate) and model
loadability cannot be proven before exec, so that path stays recoverable via
reset-password.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: harden reset-password ordering and validate the in-venv backend before the strip

Three follow-ups to the pre-exposure hardening:

reset-password now deletes auth.db FIRST and proves it is gone before touching
the seeded credential files. If the DB cannot be removed (a running Studio or
Windows holds it open, or a read-only auth dir) it aborts with the credential
files untouched, so a forgotten-password reset is not left half-done with the
recovery credentials deleted while an un-resettable must_change_password=1 DB
survives. After the DB is gone it invalidates the stale credential files
(unlink, else truncate) and fails closed if a file can be neither removed nor
truncated, since a surviving plaintext would be re-seeded by
generate_bootstrap_password() and re-validate the revoked password.

The in-venv (in-process) launch path had no analogue of the re-exec launcher
check: a headless public launch would seed the admin and strip the seeded
.bootstrap_password in the gate before _load_run_module() later failed on a
broken/partial venv, leaving must_change_password=1 with no password to log in.
Add _validate_inproc_backend_before_strip, called on the in-venv path (both
`unsloth studio` and `unsloth studio run`) before the gate on the headless
public path, so a broken backend fails cleanly before anything is stripped. It
is scoped to the headless path so an interactive prompt is not delayed behind a
full backend import.

* Studio: validate the frontend and tunnel before the strip on every public path

Five follow-ups closing the remaining pre-exposure-strip lockouts:

The in-venv (in-process) paths of both `unsloth studio` and `unsloth studio
run` validated the backend but not the frontend before the gate, so a headless
public launch with a missing/bad dist would strip the seeded .bootstrap_password
and then abort in run_server() during frontend setup, leaving
must_change_password=1 with no login page. Both now validate a servable frontend
before the strip (cheap check first, backend import after) and serve the
resolved dist in-process.

The `studio run` re-exec discarded the dist that satisfied the pre-strip check
and only forwarded a user-supplied --frontend. In a shadowed install where the
parent finds a built dist the child cannot, it stripped and exec'd without the
path, and the child aborted during frontend setup. It now forwards the resolved
dist, matching `unsloth studio`.

On a headless --secure launch the bind is loopback, so the Cloudflare tunnel is
the only public exposure. If cloudflared is provably unavailable (found nowhere
and undownloadable) the tunnel cannot start, so stripping the recovery
credential would just lock the user out with no public URL ever served. Add
_tunnel_binary_confirmed_unavailable and, on --secure only, refuse the launch
with the credential preserved rather than strip. Wildcard --cloudflare binds
0.0.0.0 publicly regardless of the tunnel, so it still strips; any uncertainty
(helper not loadable) also still strips, since a possible credential leak
outweighs a recoverable lockout.

clear_bootstrap_password no longer claims it cleared the file's contents when
both unlink and truncate failed; it now reports the stale password is still on
disk and asks the user to remove it manually.

* Studio: fix cloudflared probe path and skip the bootstrap strip for a self-suppressing child

Two follow-ups to the --secure pre-exposure hardening:

The cloudflared availability probe loaded cloudflare_tunnel by file path but not
its backend deps: ensure_cloudflared() -> _cache_path() lazily imports
utils.paths.storage_roots, which only resolves when studio/backend is on
sys.path. From the outer CLI it is not, so the probe saw ensure_cloudflared()
return None (cache unresolvable) and wrongly treated the tunnel as unavailable,
refusing --secure even when cloudflared was cached or downloadable. Add the
backend dir to sys.path for the probe (and remove it after) so the cache path
resolves as it will in the child.

A headless --secure launch stripped the seeded .bootstrap_password before the
child proved the tunnel could actually connect, so a cloudflared that is present
but cannot establish the tunnel (blocked connectivity, Cloudflare outage) left
must_change_password=1 with no recovery credential. But the strip is only needed
when the re-exec'd child is an OLD studio-venv backend with no pre-bind
suppression: this install's own run.py sets app.state.suppress_bootstrap_injection
before binding and never serves the seeded credential publicly. Add
_child_self_suppresses (true in-process, or when the re-exec target is this
install's own run.py by path identity) and skip the strip in that case, keeping
.bootstrap_password as a local recovery credential; the strip stays fully in
force for the studio-venv console-script path and any venv-fallback run.py, where
an old child is actually possible.

* Studio: reword the pre-exposure terminal password prompt

* Studio: warn when -H is overridden by --secure; align pre-exposure prompt wording

- --secure/--secure run: emit a Note (not an error) when -H is a non-loopback
  host, since --secure forces the loopback bind and would otherwise discard -H
  silently.
- Reword the pre-exposure terminal prompt to 'exposed on the public internet'
  in both the backend gate and the CLI mirror.
- Align the CLI success line with the backend ("Password updated for '<user>'.").
- Tests for the new -H warning (present when overridden, absent on loopback).

* Studio: add non-interactive --password to set the initial admin password

Headless hosts (CI, containers, systemd units) have no TTY, so the forced
first-exposure password change could not be completed unattended. Add a
non-interactive way to set the INITIAL admin password before the server binds:

- --password <value>, the UNSLOTH_STUDIO_PASSWORD env var, or --password -
  (read one line from stdin). Off by default; unset falls back to the normal
  interactive terminal prompt / browser setup.
- Applies on any launch (public --secure/--cloudflare or a headless -H 0.0.0.0
  bind), only when the account still has its seeded bootstrap password. An
  already-set password is a hard error, never an override; an invalid value
  (too short, or equal to the bootstrap) fails closed before bind.
- The CLI applies the change in the parent, never forwards --password to the
  re-exec child, and strips UNSLOTH_STUDIO_PASSWORD from the child env so the
  secret never crosses to the child. run.py does the same on the direct path and
  strips the env var so spawned subprocesses (cloudflared, llama-server, tools)
  cannot inherit it.

Mirrors resolve_supplied_password across the CLI and backend, documents the
option in the README (including the argv-visibility caveat), and covers all
flows (env/stdin/literal, fail-closed cases, no-forward, env-strip,
reset-password roundtrip) in the CLI, backend, and unit suites.

* Studio: truncate the stale bootstrap file when unlink fails on a CLI password change

The post-change cleanup in _cli_update_password only warned when
.bootstrap_password could not be unlinked but was still writable (locked file,
read-only auth dir), leaving the old plaintext on disk. If auth.db is later
reset or removed, generate_bootstrap_password() reads that file back and
re-validates the revoked bootstrap password. Truncate the file on unlink
failure so its stale plaintext cannot be re-seeded, mirroring the backend
clear_bootstrap_password(); the password change is already committed, so this
never rolls it back. The warning now states truthfully whether the contents
were cleared or the file must be removed manually.

* Studio: tighten comments

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-15 06:13:25 -07:00
Daniel Han
ca979e9643
Studio: add UNSLOTH_SKIP_AUTOSTART installer flag (#7093)
* Studio: add installer autostart opt-out

* CI: run installer autostart tests cross-platform

* Tests: combine Studio installer skip flags
2026-07-12 21:23:14 -07:00
oobabooga
b0b8aea618
Clarify in README that -H 0.0.0.0 starts a public Cloudflare tunnel (#7007)
Some checks are pending
Studio GGUF CI / Tool calling Tests (push) Waiting to run
Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio API CI / Studio API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio Update CI / Studio Updating Tests (push) Waiting to run
Studio Update CI / Studio Updating Tests (push) Waiting to run
Windows Studio API CI / Studio API & Auth Tests (push) Waiting to run
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Studio UI CI / Chat UI Tests (push) Waiting to run
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Studio GGUF CI / Tool calling Tests (push) Waiting to run
Windows Studio GGUF CI / JSON, images (push) Waiting to run
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Waiting to run
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Studio UI CI / Chat UI Tests (push) Waiting to run
Windows Studio Update CI / Studio Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
* Clarify in README that -H 0.0.0.0 starts a public Cloudflare tunnel

* Hedge tunnel URL wording and restore trusted-network caution

* Tighten the 0.0.0.0 tunnel note

* Drop trust-the-network caution from tunnel note

* Restore trusted-network note on the raw-bind sentence

* Use Cloudflare's quick tunnel terminology and consolidate the trust warning
2026-07-09 17:59:48 -07:00
Luca Cesarano
4a5d41eb3d
fix(install): enable UV_NATIVE_TLS on macOS for corporate TLS-inspection proxies (#6671)
---------

Co-authored-by: Luca Cesarano <luca.cesarano@sygnum.com>
Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
2026-06-26 16:48:30 -03:00
Daniel Han
54f25bf17e
Studio: UNSLOTH_NPM_REGISTRY opt-in for corporate npm mirrors (#6491) (#6663)
* Studio: UNSLOTH_NPM_REGISTRY opt-in for corporate npm mirrors (#6491)

studio/frontend/.npmrc pins registry=https://registry.npmjs.org/ as a
supply-chain lock. A project-level pin takes precedence over a user's
~/.npmrc, so behind a corporate firewall that blocks npmjs.org the
frontend bun/npm install hit npmjs.org directly and failed with 403.

Add an opt-in UNSLOTH_NPM_REGISTRY env var (off by default). When set it
is threaded as --registry into every registry-touching install in
setup.sh, setup.ps1 and build.sh (bun bootstrap, bun install + retry, npm
fallback, OXC validator runtime). --registry is the highest-precedence
override for both bun and npm and leaves min-release-age and save-exact
in force, so the default lock is unchanged for everyone else.

On an install failure that looks like a blocked registry, print guidance
pointing at UNSLOTH_NPM_REGISTRY and auto-suggest the mirror already set
in the user's npm config. Registries are never switched automatically.

Also correct the .npmrc comment: the pin does not block an ambient
NPM_CONFIG_REGISTRY env var (npm and bun honor that at higher precedence);
it only guards against a lower-precedence stale ~/.npmrc.

* Studio: make the registry hint reachable under set -e; clean temp log (#6491)

run_quiet_no_exit returns non-zero on failure, which under `set -euo
pipefail` exits the script at the call site before the exit code is
captured, so the new UNSLOTH_NPM_REGISTRY hint never printed on the npm
fallback and OXC validator paths. Guard both with `|| _rc=$?` (the same
idiom every other run_quiet_no_exit caller already uses) so the failure
branch runs, and remove the _FRONTEND_INSTALL_LOG temp file on the
early-exit path.

* Studio: detect the user's mirror outside the pinned frontend dir (#6491)

_suggest_npm_registry / Show-NpmRegistryHint run while the cwd is still
studio/frontend, whose .npmrc pins registry=https://registry.npmjs.org/.
So `npm config get registry` returned that pin instead of the user's
~/.npmrc mirror, and the "Detected a registry" branch was skipped for the
main corporate case (mirror set in ~/.npmrc). Run the lookup from a
directory with no project .npmrc (/ in bash, the temp dir in PowerShell)
so the user/global mirror is surfaced. The NPM_CONFIG_REGISTRY env check
is unchanged and still takes precedence.
2026-06-25 04:01:43 -07:00
Daniel Han
b964d349fc
Clarify in README that studio --secure creates a public tunnel (#6632)
The Launch-section blurb described --secure as a secure HTTPS link
instead of a raw port and stressed that the raw port is never exposed,
which reads as the more private option. The Cloudflare tunnel actually
publishes Studio at a public trycloudflare.com URL, and server-side
tools let anyone with the API key run code. Reword to state the public
exposure and the code-execution caveat, matching the installer hints and
the Remote access section.
2026-06-24 04:03:12 -07:00
Daniel Han
c53456adf7 Remove Windows Blackwell tips line from README 2026-06-20 12:09:44 +00:00
Harshita
4fddae3840
docs: add Windows installation & troubleshooting guide for RTX 50-series (Blackwell) (#6286)
* Added Windows RTX 50-series troubleshooting guide

* Update README.md to link Blackwell Windows Troubleshooting Guide

Added a troubleshooting guide for Windows installation issues with RTX 50-Series.

* Fix install path, troubleshooting accuracy and markdown for PR #6286

* Condense Windows Blackwell tips into README and drop standalone guide for PR #6286

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-06-20 03:49:45 -07:00
Daniel Han
b094bf8590
README: document unsloth studio --secure (HTTPS) in the Launch section (#6500)
Some checks are pending
Backend CI / (Python 3.10) (push) Waiting to run
Backend CI / (Python 3.11) (push) Waiting to run
Backend CI / (Python 3.12) (push) Waiting to run
Backend CI / (Python 3.13) (push) Waiting to run
Backend CI / Repo tests (CPU) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Studio GGUF CI / Tool calling Tests (push) Waiting to run
Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio API CI / Studio API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Studio Updating Tests (push) Waiting to run
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Studio UI CI / Chat UI Tests (push) Waiting to run
Studio Update CI / Studio Updating Tests (push) Waiting to run
Windows Studio API CI / Studio API & Auth Tests (push) Waiting to run
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Studio GGUF CI / Tool calling Tests (push) Waiting to run
Windows Studio GGUF CI / JSON, images (push) Waiting to run
Windows Studio UI CI / Chat UI Tests (push) Waiting to run
Windows Studio Update CI / Studio Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
2026-06-20 01:23:57 -07:00
Daniel Han
3452b91764
README: Studio developer install tracks main (nightly), add UNSLOTH_STUDIO_HOME and --secure (#6498)
* README: Studio developer install tracks main (nightly), add UNSLOTH_STUDIO_HOME and --secure

* README: rename heading to Developer / Nightly / Experimental installs
2026-06-20 00:14:19 -07:00
Daniel Han
92e7563fa2
Document install env vars in README advanced launch options (#5972)
Add copyable per-platform examples for UNSLOTH_NO_TORCH, UNSLOTH_PYTHON and
UNSLOTH_STUDIO_HOME (curl | sh after the pipe; $env: before irm | iex), and
move the UNSLOTH_CPU_THREADS note to the end of the section.
2026-06-03 05:39:38 -07:00
Michael Han
6bf101107a
Removing unsloth studio update.md
Some checks are pending
Backend CI / (Python 3.10) (push) Waiting to run
Backend CI / (Python 3.11) (push) Waiting to run
Backend CI / (Python 3.12) (push) Waiting to run
Backend CI / (Python 3.13) (push) Waiting to run
Backend CI / Repo tests (CPU) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Studio GGUF CI / Tool calling Tests (push) Waiting to run
Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio API CI / Studio API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Studio Updating Tests (push) Waiting to run
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Studio UI CI / Chat UI Tests (push) Waiting to run
Studio Update CI / Studio Updating Tests (push) Waiting to run
Windows Studio API CI / Studio API & Auth Tests (push) Waiting to run
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Studio GGUF CI / Tool calling Tests (push) Waiting to run
Windows Studio GGUF CI / JSON, images (push) Waiting to run
Windows Studio UI CI / Chat UI Tests (push) Waiting to run
Windows Studio Update CI / Studio Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
2026-05-31 07:31:27 -07:00
Daniel Han
998feaf9d0 Update README.md 2026-05-31 07:29:24 -07:00
mervivian
faea796c35
fix: wrong code block language for nightly Windows install (#5877)
Some checks are pending
Security audit / npm scan-packages (Studio frontend tarballs) (push) Waiting to run
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Waiting to run
Security audit / pytest tests/security (push) Waiting to run
Security audit / npm provenance + new install-script diff (push) Waiting to run
Studio API CI / Studio API & Auth Tests (push) Waiting to run
Backend CI / (Python 3.10) (push) Waiting to run
Backend CI / (Python 3.11) (push) Waiting to run
Backend CI / (Python 3.12) (push) Waiting to run
Backend CI / (Python 3.13) (push) Waiting to run
Backend CI / Repo tests (CPU) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Studio GGUF CI / Tool calling Tests (push) Waiting to run
Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio API CI / Studio API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Studio Updating Tests (push) Waiting to run
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Studio UI CI / Chat UI Tests (push) Waiting to run
Studio Update CI / Studio Updating Tests (push) Waiting to run
Windows Studio API CI / Studio API & Auth Tests (push) Waiting to run
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Studio GGUF CI / Tool calling Tests (push) Waiting to run
Windows Studio GGUF CI / JSON, images (push) Waiting to run
Windows Studio UI CI / Chat UI Tests (push) Waiting to run
Windows Studio Update CI / Studio Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
Co-authored-by: danielhanchen <michaelhan2050@gmail.com>
2026-05-30 21:15:40 -07:00
Daniel Han
131fa4fcae
Trim README Advanced launch options blurb (#5809) 2026-05-27 05:15:45 -07:00
alkinun
9222ffd9b4
Studio: add configurable CPU thread pool limit (#5760)
* Studio: add configurable CPU thread pool limit

* Studio: report invalid CPU thread setting cleanly

* Studio: also cover uvicorn main:app, harden tests, move docs to advanced

---------

Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
2026-05-27 05:09:34 -07:00
Michael Han
b180ae7dd6
Adding Connect a Provider README changes 2026-05-21 05:08:59 -07:00
Daniel Han
a74a1080e0
Move uninstall scripts into scripts/ and fix references (#5644)
Some checks are pending
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Security audit / npm scan-packages (Studio frontend tarballs) (push) Waiting to run
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Waiting to run
Security audit / pytest tests/security (push) Waiting to run
Security audit / npm provenance + new install-script diff (push) Waiting to run
Studio API CI / Studio API & Auth Tests (push) Waiting to run
Backend CI / (Python 3.10) (push) Waiting to run
Backend CI / (Python 3.11) (push) Waiting to run
Backend CI / (Python 3.12) (push) Waiting to run
Backend CI / (Python 3.13) (push) Waiting to run
Backend CI / Repo tests (CPU) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Studio GGUF CI / Tool calling Tests (push) Waiting to run
Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio API CI / Studio API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Update CI / Studio Updating Tests (push) Waiting to run
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Studio UI CI / Chat UI Tests (push) Waiting to run
Studio Update CI / Studio Updating Tests (push) Waiting to run
Windows Studio API CI / Studio API & Auth Tests (push) Waiting to run
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Studio GGUF CI / Tool calling Tests (push) Waiting to run
Windows Studio GGUF CI / JSON, images (push) Waiting to run
Windows Studio UI CI / Chat UI Tests (push) Waiting to run
Windows Studio Update CI / Studio Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
* Move uninstall scripts into scripts/ and fix all references

Relocates `uninstall.sh` and `uninstall.ps1` from the repo root into
the existing `scripts/` directory, alongside the other helper scripts.

Reference fixes:
* `README.md`: Studio uninstall instructions now point at the raw
  GitHub URLs under `scripts/`. The previous `unsloth.ai/uninstall.*`
  short URLs currently 404 (unlike `unsloth.ai/install.sh`, which
  301s to the raw github URL), so the raw URL is the working entry
  point until that redirect is configured.
* `scripts/uninstall.sh` header `Usage:` example updated to the new
  raw GitHub path.
* `scripts/uninstall.ps1` header `Usage:` example updated to the new
  raw GitHub path.
* `.github/workflows/studio-update-smoke.yml`: `paths:` trigger and
  round-trip exec/exists checks now use `scripts/uninstall.sh`.
* `.github/workflows/studio-mac-update-smoke.yml`: same.
* `.github/workflows/studio-windows-update-smoke.yml`: `paths:`
  trigger and round-trip exec/exists checks now use
  `scripts/uninstall.ps1`.

The in-script help hints (e.g. `sh uninstall.sh`, `.\uninstall.ps1`)
are left unchanged because they are user-facing examples shown after
the user already has the file locally, and the basename form works
regardless of which directory the user downloaded the script into.

Follow-up note for unsloth.ai: once this lands, please add the
`unsloth.ai/uninstall.sh` and `unsloth.ai/uninstall.ps1` short-URL
redirects to `raw.githubusercontent.com/unslothai/unsloth/main/scripts/...`
(matching the existing `unsloth.ai/install.sh` redirect pattern).

* Update remaining uninstall script help hints for new scripts/ path

Three user-facing strings inside the uninstall scripts still showed
the old basename form, which became misleading after the move:

* `scripts/uninstall.ps1` header `# Local:` example: now references
  `.\scripts\uninstall.ps1` (the actual path from the cloned repo
  root).
* `scripts/uninstall.sh` env-var re-run hint: now shows the canonical
  curl-pipe form documented in README, since callers who came via
  `curl -fsSL ... | sh` never had a local `uninstall.sh` to invoke.
* `scripts/uninstall.ps1` env-var re-run hint: same, switched to the
  `irm ... | iex` form documented in README.

Pure string changes, no behavior change.

---------

Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
2026-05-20 04:42:03 -07:00
Daniel Han
47167885b0
studio: add uninstall.ps1 for Windows (#5513)
* studio: add uninstall.ps1 and document it in README for Windows

The previous Windows uninstall guidance was Remove-Item -Recurse -Force on
$HOME\.unsloth\studio, which only deletes the install dir and leaves
behind:

  * %LOCALAPPDATA%\Unsloth Studio                 (data dir)
  * Desktop\Unsloth Studio.lnk                    (Desktop shortcut)
  * %APPDATA%\Microsoft\Windows\Start Menu\Programs\Unsloth Studio.lnk
  * Custom UNSLOTH_STUDIO_HOME / STUDIO_HOME roots
  * Running unsloth_studio venv processes
  * User PATH entry under .unsloth\studio
  * HKCU\Software\Unsloth\PathBackup

This script mirrors uninstall.sh for Windows. It stops listening backends
by reading the port from share\studio.port (with a Win32_Process sweep
anchored on \unsloth_studio\ as a fallback), removes the install dir,
data dir, both shortcuts, the Studio PATH entry, and the PathBackup
registry key. Custom roots discovered from env vars or share\studio.conf
are accepted only if they contain a Studio sentinel (share\studio.conf,
unsloth_studio\.unsloth-studio-owned, or bin\unsloth.exe) and are not
on a hard deny list (drive root, %USERPROFILE%, parent of %USERPROFILE%,
or top-level system paths).

README now points Windows users at the script.

* Scope port-file kill and PATH cleanup to known Studio roots for PR #5513

Three findings from the reviewer round:

1. _StopByPortFile killed whatever owned the recorded port without proving
   the PID belonged to this Studio install. A stale studio.port pointing
   at a port a different local service later bound would force-kill that
   service. New _PidUnderKnownRoot checks the listening PID's exe path
   against the same $KnownRoots that _StopStudioProcesses already uses.

2. The netstat.exe fallback matched ":$port " anywhere in the line, so a
   stale port file with 443 (or any common port) could match an
   ESTABLISHED row whose remote endpoint was that port, killing an
   unrelated process (browser, IDE). Now requires the row contain
   LISTENING, and applies the same _PidUnderKnownRoot ownership check.

3. PATH cleanup removed any entry whose expanded path contained
   \unsloth_studio\, which would also clobber an unrelated user virtualenv
   that shared the name. Now only removes entries that resolve inside a
   known Studio root (default %USERPROFILE%\.unsloth\studio plus any
   custom roots discovered from UNSLOTH_STUDIO_HOME / STUDIO_HOME /
   share\studio.conf).

* Expand tilde and honor UNSLOTH_STUDIO_HOME precedence for PR #5513

Two findings from the latest review round:

1. install.ps1 (lines 152-154) expands ~ and ~\path to $env:USERPROFILE
   before resolving the install root, but uninstall.ps1 was passing the
   raw env value to [System.IO.Path]::GetFullPath. That resolved ~\foo
   relative to the current directory rather than the user profile, so a
   user who installed with UNSLOTH_STUDIO_HOME='~\custom' could not
   uninstall through the same variable. New _ExpandTilde helper matches
   install.ps1's behavior.

2. Mirror install.ps1's env-var precedence: UNSLOTH_STUDIO_HOME wins,
   STUDIO_HOME is ignored when both are set. Otherwise uninstalling
   install A could also touch install B if the user has a stale
   STUDIO_HOME pointing at B.

---------

Co-authored-by: Daniel Han <info@unsloth.ai>
2026-05-18 02:32:24 -07:00
Michael Han
c41ce170ec
studio: add uninstall.sh and document it in README (#5497)
* studio: add uninstall.sh and document it in README

The current uninstall guidance in README.md is `rm -rf ~/.unsloth/studio`,
which leaves behind everything that lives outside that path:

  - ~/.local/share/unsloth/ (launcher script, studio.conf, studio.log,
    icon assets)
  - ~/Applications/Unsloth Studio.app (macOS bundle, orphaned and
    pointing nowhere on next reinstall)
  - ~/Desktop/Unsloth Studio (broken symlink after the bundle is gone)
  - ~/Desktop/unsloth-studio.desktop (Linux)
  - ~/.local/share/applications/unsloth-studio.desktop (Linux)
  - /tmp/unsloth-studio-launcher-<uid>*.lock (lock dir, possibly stale)
  - Launch Services cache entry for ai.unsloth.studio on macOS
  - Any running `unsloth studio -p N` processes

Users who follow the documented uninstall and reinstall end up with the
new launcher layered on top of stale state from the previous install,
which has produced concrete bugs (e.g. self-referential symlink inside
the .app bundle after a reinstall over leftover state).

Add uninstall.sh at the repo root that handles all of the above, and
update README.md to point at it as the recommended path. The plain
`rm -rf ~/.unsloth/studio` line is kept as a "partial uninstall, keep
launcher for a later reinstall" alternative. The model cache at
~/.cache/huggingface is intentionally left untouched, with a note in
the script suggesting how to remove it if desired.

Script is POSIX sh, idempotent (every removal is gated on existence
and uses `2>/dev/null || true`), and handles macOS, Linux, and WSL.
Windows is intentionally not covered here; the existing PowerShell
Remove-Item line in README is kept for that.

* studio: trim uninstall.sh header

* studio: address PR review feedback on uninstall.sh

Four findings from automated review, all verified real:

1. pkill pattern only matched `-p N`, not `--port N`. Studio
   instances launched with the long option form survived the
   uninstall. Fix: run two pkill passes, one for each form, with
   `[ =]` covering both space and `=` separators.

2. CLI shim at ~/.local/bin/unsloth (symlink into the venv created
   by install.sh:2167) was left behind, becoming a broken symlink
   after the venv directory is removed. Fix: add it to the removals.

3. Custom install roots via UNSLOTH_STUDIO_HOME / STUDIO_HOME were
   not removed. install.sh records the install location in
   ~/.local/share/unsloth/studio.conf as UNSLOTH_EXE; parse it,
   derive the root as three dirnames up, and remove the root if it
   is non-default.

4. On WSL the installer creates 'Unsloth Studio.lnk' on the Windows
   Desktop and Start Menu Programs folder via powershell.exe.
   Mirror that path on uninstall by invoking powershell.exe to
   Remove-Item the same two locations. Best-effort, gated on
   powershell.exe being available.

Tests (T2.8b, T2.15, T2.16, T2.17, T2.18, T2.5b) added behind the
scenes; all pass on macOS Darwin 25.3 with `dash -n`, `sh -n`,
shellcheck-clean (SC2016 suppressed on the PowerShell single-quoted
heredoc since the $env: expansions must remain literal to the
shell so PowerShell receives them verbatim).

* studio: harden uninstall.sh against env-mode and shim collisions

- Honor UNSLOTH_STUDIO_HOME / STUDIO_HOME at uninstall time and read
  env-mode studio.conf at $<root>/share/studio.conf, not just the
  default-mode conf under $HOME/.local/share/unsloth/. Without this,
  installs done with a custom STUDIO_HOME leak the install tree even
  when the env var is re-exported.
- Guard the custom-root resolver against "/" and empty so a corrupted
  studio.conf (UNSLOTH_EXE='/etc/passwd' or similar) or an
  UNSLOTH_STUDIO_HOME=/ cannot trick the script into rm -rf'ing root.
- Only remove $HOME/.local/bin/unsloth when it is a symlink resolving
  to a Studio venv. pyproject.toml declares unsloth as a console
  script, so pip install --user unsloth places a regular file at the
  same path; the previous unconditional rm wiped that unrelated CLI.
- When neither env var is set, print a tail hint so users with custom
  install roots know to re-run with the variable.

Verified with a sandboxed harness covering 24 scenarios (default and
env-mode installs across macOS / Linux / WSL, idempotency, hostile
lockfile names, path-traversal attempts, malformed conf, pkill long
and short forms, pip-conflict shim, broken-symlink bundle path).
Script remains POSIX (shellcheck -s sh clean, runs under /bin/dash).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Refuse non-Studio uninstall roots and tighten process matching for PR #5497

Three issues found while testing custom-root paths and process cleanup:

1. UNSLOTH_STUDIO_HOME=$HOME sh uninstall.sh rm -rf'd $HOME (same for
   STUDIO_HOME and parent-of-$HOME). install.sh accepts any writable
   directory for STUDIO_HOME, so the uninstaller must validate ownership
   before deletion. _is_studio_root accepts a candidate root only if it
   contains share/studio.conf, an unsloth_studio/ directory, or a
   bin/unsloth shim pointing into unsloth_studio/bin. _is_unsafe_root is
   a defense-in-depth deny list (/, $HOME, $HOME's parent, system paths).

2. pkill -f patterns "unsloth studio.*-p[ =][0-9]" over-matched on argv
   substrings. A user running `less notes.md` whose filename contained
   "unsloth studio ... -p N" had their less killed. New patterns anchor
   on /unsloth_studio/bin/ so only processes whose actual exe lives in a
   Studio venv match.

3. pkill missed processes that exec into studio/backend/run.py --port N
   (the post-exec form when the unsloth CLI replaces itself). Added a
   third pattern for that shape, and prefer PID files written by
   install.sh's _spawn_terminal (studio-$port.pid in DATA_DIR) over
   argv matching for installs that have them.

* Tighten ownership guards from review round for PR #5497

Three findings from the second reviewer round:

1. _is_studio_root accepted any directory containing an unsloth_studio/
   subdir as Studio-owned. A user workspace that happens to contain a
   folder named unsloth_studio/ would be deleted. install.sh's env-mode
   guard at install.sh:1358-1361 already requires .unsloth-studio-owned
   before treating the venv as replaceable. Mirror that: require the
   owner marker, share/studio.conf, or the bin/unsloth shim target.

2. The pkill -f fallback patterns were global, so uninstalling install A
   would also kill install B's running server. Scope each pattern to the
   actual install root being removed by interpolating the root path into
   the regex. Also adds a third pattern shape for `unsloth studio` with
   no -p / --port flag (the CLI default-port form).

3. Desktop/Unsloth Studio is created by install.sh as a symlink to the
   .app bundle. If a user has a regular directory by that name (photos,
   notes, etc.), the previous _remove_path call rm -rf'd it. Now we only
   remove it when it is a symlink or does not exist.

* Canonicalize env roots and honor UNSLOTH_STUDIO_HOME precedence for PR #5497

Two findings from the latest review round:

1. Canonicalize env-derived roots before the safety check. The deny list
   only string-compares against $HOME, so a syntactic variant like
   UNSLOTH_STUDIO_HOME=$HOME/../$USER (or trailing slash, or relative
   path) bypassed _is_unsafe_root even though it resolves to $HOME. Now
   _emit runs CDPATH= cd -P -- + pwd -P first, so all variants normalize
   to the same canonical path before the deny check. Also added the same
   tilde expansion install.sh's _resolve_studio_destinations does.

2. Mirror install.sh's env-var precedence (install.sh:282-290). When
   both UNSLOTH_STUDIO_HOME and STUDIO_HOME are set, install.sh resolves
   only UNSLOTH_STUDIO_HOME and ignores STUDIO_HOME. Uninstall was
   emitting both, so running uninstall.sh for install A would also
   delete install B if the user had a stale STUDIO_HOME pointing at B.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Daniel Han <info@unsloth.ai>
2026-05-18 02:11:05 -07:00
Michael Han
9bab3b955e
Add API Inference endpoint 2026-05-05 06:13:35 -07:00