Commit graph

25 commits

Author SHA1 Message Date
Daniel Han
1daaa5cbb4
Let a decode failure degrade instead of escaping a fail-closed helper (#7487)
* Let a decode failure degrade instead of escaping a fail-closed helper

Pinning utf-8 makes a read that used to return mojibake on Windows raise
instead. 33 of those reads sit under a handler catching OSError or
json.JSONDecodeError but not UnicodeDecodeError, which subclasses
ValueError, so a corrupt file would now escape a helper written to return
a default. Adds UnicodeDecodeError to those tuples only.

* Treat an undecodable install lock as stale instead of retrying forever
2026-07-27 03:26:08 -07:00
Daniel Han
3fd948eb95
Pin utf-8 on shipping-code text I/O instead of the operator locale (#7486)
* Pin utf-8 on shipping-code text I/O instead of the operator locale

113 read_text/write_text/open call sites across unsloth, studio and
unsloth_cli let locale.getencoding() decide the encoding. That is utf-8 on
the Linux and macOS runners and cp1252 on a stock Windows install, so the
same file decodes differently for a Windows user and silently produces
mojibake or raises UnicodeDecodeError.

Adds tests/test_runtime_text_encoding.py to keep it that way. It resolves
openers through each file's own imports rather than a fixed list of module
names, so an aliased tarfile.open or a local from PIL.Image import open is
not asked for an encoding it does not take.

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

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

* Scan tracked files only and resolve the unbound Path calling forms

* Honour PEP 263 when scanning sources and migrate a legacy JSONL before appending

* Scope guard imports lexically and only migrate a legacy file when it round-trips

* Leave a legacy JSONL untouched and resolve path aliases in the foreign-opener check

* Tighten comments

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-27 02:14:20 -07:00
Daniel Han
c4b777263d
fix(studio/colab): fix OutStream startup crash and tidy the notebook cards (#7404)
* fix(studio/colab): survive ipykernel OutStream close() during startup

Unsloth Studio crashed at server startup on Colab with:

  Unsloth Studio failed to start: 'OutStream' object has no attribute
  'watch_fd_thread'

Root cause:
- Colab's ipykernel OutStream is created with watchfd=False, so it never
  gains a watch_fd_thread. The OutStream.close() in the affected ipykernel
  versions joins that thread unconditionally and raises AttributeError
  (ipython/ipykernel#867).
- _setup_server_disk_logging() replaces sys.stdout/sys.stderr with a tee.
  That changes the console object identity, so Colab's absl logging handler
  (which captured the original OutStream and whose close() deliberately skips
  sys.stdout/sys.stderr) no longer treats it as the live console.
- run_server builds uvicorn.Config(...), whose configure_logging runs
  logging.config.dictConfig -> logging.shutdown, closing every existing
  handler. The absl handler then calls close() on the orphaned OutStream and
  the AttributeError propagates out of uvicorn.Config and aborts startup.

Fix:
- Before installing the tee, harden the displaced console streams' close() so
  only the ipykernel#867 AttributeError is swallowed; a healthy close() runs
  unchanged and any other error still propagates. The buggy close() raises
  before it nulls pub_thread, so the stream stays fully usable.
- Give _TeeStream its own close() that flushes the log copy and forwards
  close() to the wrapped console stream best-effort, so a handler that
  captured the tee cannot crash startup either.

Add regression tests reproducing the exact path (an absl-style handler closing
a watchfd=False OutStream stand-in during logging.shutdown) and asserting the
tee/console path survives and keeps logging.

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

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

* Show the Colab login password in the shareable link card

* Tighten Colab card comments for PR #7404

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

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

* Make the Colab tunnel URL clickable and emphasise the password

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

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

* Narrow the console close() hardening to the watch_fd_thread AttributeError

* Put the Colab password on its own line so selection excludes the label

* Keep the Colab password as plain selectable text

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-25 04:11:03 -07:00
Souravrajvi0
d17567af3e
fix(studio/colab): restore blank Colab iframe embed (#7344) (#7349)
* fix(studio/colab): restore iframe embed via serve_kernel_port_as_iframe

Colab's output sanitizer often strips custom <iframe> tags from
IPython.display.HTML without raising, leaving a blank cell even though
display() succeeded. The kernel-port helper is the supported embedding
path and registers the proxy correctly.

- Prefer serve_kernel_port_as_iframe; keep raw HTML iframe as fallback
- Always show the clickable link card via show_link() so the proxy URL
  is visible even when iframe embedding fails
- Add regression tests for embed ordering and URL truncation

Fixes #7344

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

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

* fix(studio/colab): harden iframe embed fallbacks per Codex review

Guard show_link so a display failure cannot skip embedding, and only use
serve_kernel_port_as_iframe when get_colab_url returned a real Colab proxy
URL so localhost/colabtools environments still get the HTML iframe path.

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

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

* fix(studio/colab): stop opening Colab proxy URLs in a new tab (#7349)

Colab *.prod.colab.dev proxy hosts are session-scoped and return HTTP 404
when opened as a top-level tab or from another device. Replace the
clickable Open button for those URLs with an in-notebook ready card, keep
serve_kernel_port_as_iframe for the UI, and point users at
start(cloudflare=True) for a real shareable / new-window link.

* fix(studio/colab): use kernel iframe on real Colab when eval_js fails (#7349)

Gate serve_kernel_port_as_iframe on COLAB_RELEASE_TAG + google.colab import
instead of a successful proxyPort URL. When eval_js fails and get_colab_url
falls back to localhost, real Colab notebooks still embed via the kernel helper
(port-only). colabtools without COLAB_RELEASE_TAG keeps the HTML iframe path.

Thanks @mfielding92 for the runtime diagnosis.

* Mock top-level google package in Colab embed tests

* test(studio/colab): mock top-level google package in Colab tests

Patching only sys.modules["google.colab"] fails when no google namespace
is installed: import google.colab resolves the parent first and returns
False in _is_colab_runtime(). Add a shared helper that mocks both google
and google.colab for deterministic tests across environments.

* Tighten comments in Colab embed helpers and tests

* fix(studio/colab): default Cloudflare on Colab with durable login credentials

Colab proxy iframes often load an empty document even when the kernel helper
appends the frame, leaving users unable to reach Studio to change the bootstrap
password and blocking start(cloudflare=True).

On real Colab runtime:
- Default cloudflare to True (pass cloudflare=False to opt out)
- Finalize the random admin password and print credentials in the notebook
- Persist credentials across cell re-runs after interrupt
- Show Cloudflare link before login credentials; skip blank proxy iframe when ready
- Reuse main._IS_COLAB for runtime detection (not COLAB_RELEASE_TAG alone)
- Only trust serve_kernel_port_as_iframe on real Colab; colabtools falls back to HTML
- Keep embedding when the link card display fails

Addresses Codex review feedback on #7349 and @mfielding92's catch-22 report.

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

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

* fix(studio/colab): skip credential finalize when cloudflare=False

Only call _finalize_colab_admin_password() when opening a Cloudflare
tunnel. start(cloudflare=False) should not clear the bootstrap-password
gate or show a login card that references a missing tunnel link.

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

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

* fix(studio/colab): drop stale cached Colab credentials after password change

On a Colab rerun the finalize path redisplayed the cached first-run
password whenever the bootstrap gate was already cleared. If the admin
changed the password through the app, that cached copy no longer
authenticates, so the notebook printed dead credentials. Validate the
cached password against the current stored hash before redisplaying and
drop the cache when it no longer matches.

* [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: Daniel Han <danielhanchen@gmail.com>
2026-07-24 02:23:24 -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
Leo Borcherding
c86165e735
Studio Colab: opt-in shareable Cloudflare tunnel link (#6684)
* Studio Colab: add opt-in shareable Cloudflare tunnel link

colab.start(cloudflare=True) opts in to a free Cloudflare quick tunnel and
shows a trycloudflare.com link above the proxy iframe, reachable from any
device. Default OFF: bare start() keeps the in-tab Colab-proxy behavior.
run_server suppresses the tunnel on Colab by design, so colab.py starts it
directly via cloudflare_tunnel.start_studio_tunnel(); failures degrade to
the Colab proxy only.

* Studio Colab notebook: surface opt-in cloudflare=True in start cell

* Studio Colab: reskin shareable Cloudflare link to match the proxy banner

Retrofit _shareable_link_html to reuse the original Colab proxy banner skin
from show_link (white card, black border, Unsloth gem, black Open button)
instead of the plain dark box, so the shareable Cloudflare link gets the same
prominent 'Ready!' treatment.

* Studio Colab: address review feedback on Cloudflare tunnel

- try/finally around tunnel start + embed + keepalive so a KeyboardInterrupt
  while the tunnel is starting or the iframe is rendering tears it down instead
  of orphaning the cloudflared process (Gemini review).
- Publish the directly-started tunnel URL onto app.state.cloudflare_url via a new
  _publish_cloudflare_url helper so /api/health advertises it; otherwise the
  frontend's API examples fall back to the unreachable raw server_url (Codex P2).
  _stop_cloudflare_tunnel now also clears it so health stops showing a dead tunnel.
- Notebook: make cloudflare=True a replacement for start(), not an addition, since
  start() blocks and the second call would never run if both are left in (Codex P2).

* Studio Colab: gate Cloudflare tunnel on auth + honor opt-out in run_server

- Refuse to open the Cloudflare tunnel while the admin still holds its seeded
  bootstrap password. While requires_password_change is true the server injects
  that password into same-origin index GETs, and a public tunnel request counts
  as same-origin, so sharing the link would leak admin access. New
  _bootstrap_password_pending() gate (fails safe) blocks the tunnel and tells the
  user to change the password first, then re-run start(cloudflare=True) (P1).
- Pass cloudflare=False into run_server so the opt-out holds even when Colab
  detection fails; this helper is now the sole owner of the tunnel decision,
  preventing run_server from opening a tunnel on the 0.0.0.0 bind by default (P2).

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

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

* Studio Colab: drop duplicate tunnel link log and simplify start cell guidance

* Studio Colab: validate /api/health identity before reusing or tunneling a port

* Studio Colab: condense verbose docstrings and comments

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-06-26 00:56:23 -07:00
Daniel Han
187144d4e7
Reduce and tighten code comments and docstrings repo-wide (#6095)
Trim and tighten code comments and docstrings across the repository. Comment-only: every changed file verified code-identical to main via AST/token comparison.
2026-06-08 23:09:51 -07:00
Daniel Han
8292e699e4
Studio: make code comments and docstrings more succinct (#6029)
Trim and tighten code comments and docstrings across studio/ Python. Comment-only: every changed file verified code-identical to main via AST/token comparison.
2026-06-08 23:07:28 -07:00
Daniel Han
3ce187da02
Formatting: ruff line-length 100, kwarg-spacing passes, drop blank after short local imports (#6079)
Raise ruff line-length to 100 and extend the local pre-commit format pipeline (def-signature magic-comma normalization, short multi-line assert collapse, kwarg '=' spacing, blank-line-after-short-import removal, adjacent string-literal / f-string+plain merge, redundant-pass pruning). Every transform re-checks the file AST and is dropped if it would differ; the whole-repo reformat is verified AST-identical per file and idempotent.
2026-06-08 04:24:13 -07:00
Leo Borcherding
ee6695118c
Fix/studio colab proxy and iframe - Unsloth Studio not loading in Colab (iframe "refused to connect" and wrong URL) (#5844)
* fix(studio/colab): merge iframe+keepalive into start(), add proxy_headers to uvicorn

- Move serve_kernel_port_as_iframe and keepalive loop into colab.start()
  so both run in the same cell execution context, eliminating the race
  where the proxy URL was shown before the iframe cell had a chance to run
- Add a 2s sleep after run_server() before show_link() to give Colab's
  proxy infrastructure time to register the bound port
- Add proxy_headers=True and forwarded_allow_ips="*" to uvicorn Config
  so X-Forwarded-Proto/Host from Colab's reverse proxy are trusted
- Simplify notebook start cell (no more separate iframe cell needed)

* fix(studio/colab): fix iframe blocking and server thread crash in Colab

Two root causes for the long-standing proxy/iframe breakage:

1. SecurityHeadersMiddleware set X-Frame-Options: DENY and
   frame-ancestors 'none' unconditionally, blocking
   serve_kernel_port_as_iframe regardless of server health.
   Fix: detect Colab via COLAB_BACKEND_URL/COLAB_GPU env vars,
   relax frame-ancestors to *.prod.colab.dev and omit X-Frame-Options.

2. asyncio.run() in the daemon thread conflicted with nest_asyncio's
   global patches applied on the main thread, causing the server to
   crash silently after ready_event fired.
   Fix: use explicit new_event_loop() + run_until_complete() in the
   daemon thread to bypass nest_asyncio's asyncio.run patch.

Also replace blind time.sleep(2) with a health endpoint poll so the
link and iframe are only shown once the server is truly reachable.

* fix(studio/colab): use reliable /content + google.colab path for Colab detection

COLAB_BACKEND_URL and COLAB_GPU env vars aren't consistently set across
all Colab runtime versions. Use /content dir + google.colab package path
as a more reliable signal, computed once at module load.

* fix(studio/colab): fix port mismatch, health-check silence, and CSP framing

Four bugs causing the iframe and URL button to always fail:

1. Port not propagated back: run_server auto-increments when 8888 is taken,
   but start() kept using the original port for show_link() and
   serve_kernel_port_as_iframe() — now reads app.state.server_port.

2. Silent health-check failure: the poll loop never checked whether any
   attempt succeeded; on all-fail it continued and showed a dead link —
   now exits early with a clear error message.

3. CSP frame-ancestors too narrow: '*.prod.colab.dev' only matches one
   subdomain level; actual Colab proxy URLs are two levels deep
   (e.g. foo.region.prod.colab.dev), and the parent frame may also be
   colab.research.google.com or a sandboxed null-origin output iframe —
   changed to '*' in Colab mode (single-user sandbox, no security loss).

4. _IS_COLAB detection hardcoded python3.10/3.11 paths: Python 3.12+
   Colab runtimes wouldn't match when env vars aren't set — replaced with
   a glob over python3.*/dist-packages/google/colab.

* fix(studio/colab): harden Colab startup against every known failure mode

colab.py:
- get_colab_url: retry eval_js up to 3x (10s timeout each), validate that
  result is a real https:// URL containing the port before accepting it;
  log a clear warning when falling back to localhost
- show_link: safe short_url truncation (try/except around str.index so an
  unexpected URL shape never blocks the link card from rendering); also
  emit the URL via logger so it's visible in cell text output even if
  HTML display is suppressed
- start: detect "already running" at entry — on cell re-run Studio is
  still healthy on port 8888; skip re-launch and go straight to
  show+iframe so the user never ends up with mismatched port state
- start: wrap run_server in try/except (SystemExit + Exception) so
  startup errors surface as readable messages rather than cell crashes
- start: check frontend_path/index.html exists, not just the directory
- start: remove unused `import sys`
- start / keepalive: catch KeyboardInterrupt so interrupting the cell
  prints a clean "stopped" message instead of a raw traceback
- extract _is_studio_healthy() and _show_and_embed() helpers to
  deduplicate the fast-path and normal-path logic

main.py:
- _build_csp: in Colab mode, extend script-src to include
  *.prod.colab.dev and *.googleusercontent.com (Colab injects scripts
  from these origins into the output iframe scaffolding)
- _build_csp: in Colab mode, extend connect-src with blob:, data:,
  wss://*.prod.colab.dev, and wss://*.googleusercontent.com so
  WebSocket streams and Colab kernel traffic are not blocked by CSP

* fix(studio/colab): fix iframe width responsiveness and height sizing

Replace serve_kernel_port_as_iframe with a raw CSS iframe for two
reasons:

1. Width responsiveness: serve_kernel_port_as_iframe sets the width as
   an HTML attribute (width="100%") which Colab's output machinery can
   bake into a fixed pixel value on first render, causing the Studio to
   stop following the notebook panel width when it opens/closes or the
   window resizes. A CSS style property (style="width:100%") participates
   in normal reflow and always tracks the parent container width.

2. Height sizing: the hardcoded height=1200 was too tall on short monitors
   (forced outer-page scroll) and wasted space on tall ones. A small JS
   snippet reads screen.availHeight and sets height to ~82% of the screen,
   clamped to [600, 1100]px, with a resize listener that re-fits on zoom
   changes and panel open/close events.

Also eliminate the double eval_js call: _show_and_embed now fetches the
Colab proxy URL once and passes it to show_link via the new _url kwarg,
so google.colab.kernel.proxyPort is only called once per invocation.

Falls back to serve_kernel_port_as_iframe if IPython.display.HTML is
unavailable for any reason.

* fix(studio/colab): fix link button + add fullscreen hover button to iframe

Link button: target="_blank" is blocked by Colab's output sandbox.
Switch to onclick="window.open(url,'_blank')" which the sandbox allows.

Fullscreen: add a small button that appears on hover in the top-right
corner of the iframe. Clicking it calls requestFullscreen() on the
wrapper div and stretches the iframe to 100vh/100vw. Exits back to
normal on fullscreen change.

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

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

* revert(studio/colab): remove fullscreen button

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

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

* fix(studio/colab): address review feedback

- Wrap both urlopen calls in with statements to prevent socket/fd leaks
- Replace JS resize listener with CSS height:82vh — simpler, responsive,
  and no risk of leaked window listeners on cell re-runs
- Use importlib.util.find_spec("google.colab") instead of a glob path
  to detect Colab; more robust across Python versions and venv layouts

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

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

* fix(studio/colab): fall back to href navigation when window.open is blocked

window.open from a cross-origin sandboxed Colab output iframe can be
silently blocked by the browser (returns null, no exception). The old
code returned false unconditionally, so a blocked popup left the button
doing nothing. Now: if window.open succeeds the new tab opens and the
href is suppressed; if it returns null the browser follows the href,
navigating the output cell to Studio — always does something useful.

* fix(studio/colab): remove button, give iframe a branded header bar

The "Open Unsloth Studio" button was unreliable in Colab's sandboxed
output context regardless of how window.open was called. Since the
iframe already loads Studio inline, the button added no value and
confused users with a URL that 404s outside the output cell.

Replace the separate link card + bare iframe with a single block:
a slim black header bar (Unsloth logo + truncated URL) flush on top
of the full-height responsive iframe. Cleaner and removes the broken
button entirely.

* studio: gate uvicorn proxy_headers/forwarded_allow_ips behind _IS_COLAB

forwarded_allow_ips="*" was applied unconditionally, so every Studio
deployment trusted X-Forwarded-* headers from any client. Only Colab needs
that, because its reverse proxy fronts the kernel. For a normal
local/standalone Studio this is an unwanted relaxation, especially when bound
to 0.0.0.0.

Now proxy_headers/forwarded_allow_ips are only set when _IS_COLAB. Standalone
runs fall back to uvicorn's defaults (proxy_headers honored from loopback
only), restoring the prior security posture, while Colab keeps the wide trust
its proxy requires.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-05-28 23:54:48 -07:00
Leo Borcherding
68965988cf
Fix/studio colab button message: Add fallback message for Colab Studio button when proxy URL fails (#4866)
* Add fallback message for Colab Studio button when localhost link doesn't work

* Make fallback message darker grey for better readability

* Make fallback message bold for better visibility

---------

Co-authored-by: LeoBorcherding <LeoBorcherding@users.noreply.github.com>
2026-04-05 21:57:45 -07:00
Roland Tannous
e79a178200
Allow install_python_stack to run on Colab (#4633)
* Allow install_python_stack to run on Colab

The _COLAB_NO_VENV flag was setting _SKIP_PYTHON_DEPS=true, which
skipped both the PyPI version check (needs $VENV_DIR/bin/python) and
install_python_stack (uses sys.executable, works without a venv).

Introduce a separate _SKIP_VERSION_CHECK flag for the version check,
so install_python_stack still runs on Colab. The _SKIP_PYTHON_DEPS
flag remains available for the "versions match" fast path.

* Remove colab.py workarounds that broke transformers/hf-hub compatibility

PR #4601 added _pip_install_backend_deps(), _bootstrap_studio_venv(),
and _is_colab() to colab.py as workarounds for install_python_stack
being skipped on Colab. These workarounds:
- Stripped version constraints from studio.txt and installed into system Python
- Upgraded huggingface-hub to >=1.0, breaking Colab's pre-installed
  transformers which requires huggingface-hub<1.0

With install_python_stack now running on Colab (previous commit), these
workarounds are unnecessary — all deps are properly installed by setup.sh.
Restore colab.py to its original PR #4237 structure: just get_colab_url(),
show_link(), and start().

* Remove --local flag from setup.sh in Colab notebook

The --local flag is not needed for the standard Colab flow since
install_python_stack now runs on Colab and installs deps from PyPI.
2026-03-27 00:29:27 +04:00
Daniel Han
baabfa0a6e
Fix Colab huggingface-hub conflict, ensurepip fallback, bump to 2026.3.14 (#4603)
* Fix Colab huggingface-hub conflict, ensurepip fallback, bump to 2026.3.14

- colab.py / setup.sh: relax == pins to >= when installing studio.txt
  on Colab so huggingface-hub does not clobber Colab's bundled version
  (breaks transformers is_offline_mode import)
- install_python_stack.py: when uv is unavailable and pip is missing
  (uv-created venvs), bootstrap via ensurepip before attempting upgrade
- Bump version to 2026.3.14
- Bump installer min version pins to 2026.3.14

* [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>
2026-03-25 09:38:02 -07:00
Daniel Han
23eb7fc0a7
Fix Colab Studio launch and setup.ps1 box alignment (#4601)
* Fix Colab Studio launch and setup.ps1 box alignment

- colab.py: when the Studio venv is missing on Colab, pip-install
  backend dependencies (structlog, fastapi, etc.) from studio.txt
  into the current Python instead of failing with ModuleNotFoundError
- setup.sh: on Colab without a venv, install backend deps into system
  Python and skip venv-dependent sections (Python stack update,
  llama.cpp build) that would otherwise fail
- setup.ps1: use PadRight(47) for the done-line so "Setup Complete!"
  and "Update Complete!" both align with the box border

* [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>
2026-03-25 09:00:08 -07:00
Roland Tannous
19e9c60a8e
Consolidate dual venvs and separate install from update (#4530)
* refactor: consolidate dual venvs into single ~/.unsloth/studio/unsloth_studio

* refactor: separate install.sh (first-time) from setup.sh (smart update with PyPI version check)

* fix: install.sh calls setup.sh directly, keep both setup and update CLI commands

* fix: use importlib.resources.files() directly without _path attribute

* fix: bootstrap uv before pip upgrade to handle uv venvs without pip

* fix: frontend 404 when launched via CLI, add global symlink to ~/.local/bin

* feat: add --local flag to install.sh and unsloth studio update for branch testing

* fix: resolve repo root from script location for --local installs

* feat: add --package flag to install.sh for testing with custom package names

* feat: add --package flag to unsloth studio update

* fix: always nuke venv in install.sh for clean installs

* revert: remove Windows changes, will handle in separate PR

* fix: error when --package is passed without an argument

* revert: restore Windows scripts to current main

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

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

* fix: always explicitly set STUDIO_LOCAL_INSTALL and STUDIO_PACKAGE_NAME env vars

* fix: pass explicit STUDIO_LOCAL_REPO env var for --local installs

* fix: align banner box for Setup vs Update labels

* deprecate: hide 'unsloth studio setup' command, point users to update/install.sh

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

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

* fix: check stdout not stdin for auto-launch detection (curl pipe fix)

* fix: update install URL to unsloth.ai/install.sh

* fix: update install.sh usage comments to unsloth.ai/install.sh

* fix: use --upgrade-package for base deps to preserve existing torch/CUDA installs

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

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

* fix: --local install now also installs unsloth-zoo via base.txt before editable overlay

* fix: don't skip base packages for --local installs (editable needs unsloth-zoo)

* refactor: move --local full dep install to install.sh, keep SKIP_STUDIO_BASE for all paths

* feat: add migration support for old .venv and CWD-based installs in setup.sh

* Revert "feat: add migration support for old .venv and CWD-based installs in setup.sh"

This reverts commit 301291d002.

* feat: migrate old .venv layout in install.sh instead of always nuking

* feat: validate old .venv with torch CUDA test before migration, recovery message on launch failure

* fix: try CUDA then fall back to CPU for migration validation

* fix: upgrade unsloth/unsloth-zoo with --reinstall-package on migration to preserve torch

* remove: delete unused unsloth ui command (use unsloth studio instead)

* Fix Windows venv path mismatch between install.ps1, setup.ps1, and studio.py

install.ps1 was creating the venv CWD-relative ($VenvName = "unsloth_studio"),
setup.ps1 was using an absolute path to ".unsloth\studio\.venv", and studio.py
looks for ".unsloth\studio\unsloth_studio". All three paths were different, so
the Windows installer would never produce a working Studio setup.

install.ps1:
- Use absolute $StudioHome + $VenvDir matching the Linux install.sh layout
- Add 3-way migration: old .venv at STUDIO_HOME, CWD-relative ~/unsloth_studio
  from the previous install.ps1, or fresh creation with torch validation
- For migrated envs, upgrade unsloth while preserving existing torch/CUDA wheels
- Set SKIP_STUDIO_BASE=1 before calling setup.ps1 (matches install.sh behavior)
- Fix launch instructions to use the absolute venv path

setup.ps1:
- Change $VenvDir from ".unsloth\studio\.venv" to ".unsloth\studio\unsloth_studio"
- Add SKIP_STUDIO_BASE guard: error out if venv is missing when called from
  install.ps1 (which should have already created it)
- Differentiate "Setup" vs "Update" in banners based on SKIP_STUDIO_BASE

* setup.ps1: unconditionally error if venv missing, matching setup.sh

setup.sh always errors out if the venv does not exist (line 224-228),
telling the user to run install.sh first. setup.ps1 was conditionally
creating a bare venv with python -m venv when SKIP_STUDIO_BASE was not
set, which would produce an empty venv with no torch or unsloth. Now
setup.ps1 matches setup.sh: always error, always point to install.ps1.

* Fix --torch-backend=auto CPU solver dead-end on Linux, macOS, and Windows

On CPU-only machines, `uv pip install unsloth --torch-backend=auto`
falls back to unsloth==2024.8 because the CPU solver cannot satisfy
newer unsloth's dependencies. install.ps1 already solved this with a
two-step approach; this applies the same fix to install.sh and
install_python_stack.py.

install.sh: add get_torch_index_url() that detects GPU via nvidia-smi
and maps CUDA versions to PyTorch index URLs (matching install.ps1's
Get-TorchIndexUrl). Fresh installs now install torch first via explicit
--index-url, then install unsloth with --upgrade-package to preserve
the pre-installed torch. All 5 --torch-backend=auto removed from
primary paths.

install.ps1: add fallback else-branch when TorchIndexUrl is empty,
using --torch-backend=auto as last resort (matching install.sh).

install_python_stack.py: remove unconditional --torch-backend=auto
from _build_uv_cmd. Torch is pre-installed by install.sh/setup.ps1
by the time this runs. Callers that need it can set UV_TORCH_BACKEND.

Both install.sh and install.ps1 now share the same three-branch logic:
migrated env (upgrade-package only), normal (torch-first + index-url),
and fallback (--torch-backend=auto if URL detection fails).

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

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

* Use --reinstall-package for migrated envs on both Linux and Windows

For migrated environments (moved from legacy venv location),
--reinstall-package is better than --upgrade-package because it forces
a clean reinstall even if the same version is already installed. This
ensures proper .dist-info and .pyc state in the new venv location.

--upgrade-package remains correct for the fresh install path where
torch is already installed and we just want to add unsloth without
re-resolving torch.

* Address review findings: portability, parity, and stale comments

- Replace grep -oP (GNU Perl regex) with POSIX sed in
  get_torch_index_url() so the script works on BSD grep (macOS is
  already guarded by the Darwin early-return, but Alpine/BusyBox
  would silently get the wrong CUDA tag)
- Add LC_ALL=C before nvidia-smi invocation to prevent locale-dependent
  output parsing issues
- Add warning on stderr when nvidia-smi output is unparseable, matching
  install.ps1's [WARN] message
- Add explicit unsloth-zoo positional arg to install.ps1 migrated path,
  matching install.sh (--reinstall-package alone won't install it if it
  was never present in the migrated env)
- Fix stale comment in install_python_stack.py line 392 that still
  claimed --torch-backend=auto is added by _build_uv_cmd
- Add sed to test tools directory (function now uses sed instead of grep)

* Add --index-url to migrated env path to prevent CPU torch resolution

The migrated path runs uv pip install with --reinstall-package for
unsloth/unsloth-zoo. While uv should keep existing torch as satisfied,
the resolver could still re-resolve torch as a transitive dependency.
Without --index-url pointing at the correct CUDA wheel index, the
resolver would fall back to plain PyPI and potentially pull CPU-only
torch. Adding --index-url $TORCH_INDEX_URL ensures CUDA wheels are
available if the resolver needs them.

Applied to both install.sh and install.ps1.

* Revert --index-url on migrated env path

The original install.ps1 on main already handles the migrated path
without --index-url and it works correctly. --reinstall-package only
forces reinstall of the named packages while uv keeps existing torch
as satisfied. No need for the extra flag.

* Fix unsloth studio update --local not installing local checkout

studio.py sets STUDIO_LOCAL_REPO when --local is passed, but
install_python_stack.py never read it. The update path always
installed from PyPI regardless of the --local flag.

Add a local_repo branch that first updates deps from base.txt
(with --upgrade-package to preserve torch), then overlays the
local checkout as an editable install with --no-deps.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-25 05:24:21 -07:00
Daniel Han
100b8857f2
Fix Studio crash on Anaconda/conda-forge Python (#4484)
* Fix Studio crash on Anaconda Python due to platform._sys_version() parse failure

Anaconda and conda-forge modify sys.version to include distributor
metadata between pipe characters, e.g.:

    3.12.4 | packaged by Anaconda, Inc. | (main, ...) [MSC v.1929 ...]

Python's platform._sys_version() has a hardcoded regex that cannot
parse this format, raising ValueError. CPython closed this as "not
planned" (cpython#102396) since Anaconda modified the binary.

This breaks the import chain: run.py -> structlog -> rich -> attrs,
which calls platform.python_implementation() at module scope.

Fix: before any library imports, strip the pipe segments, parse the
cleaned version string via the standard parser, and cache the result
under the original sys.version key so all subsequent platform calls
hit the cache.

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

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

* Add defensive fallback for unpaired pipe edge cases in version patch

Address Gemini review suggestion: if the paired-pipe regex leaves
residual pipes (hypothetical single-pipe distributor metadata), fall
back to extracting the version number and the parenthesized build
info directly. Wrap the entire patch in try/except so unexpected
version string formats degrade gracefully instead of crashing the
patch itself.

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

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

* Refactor into shared _platform_compat module, cover colab.py entrypoint

Address reviewer feedback:

1. Extract the Anaconda/conda-forge sys.version fix into a shared
   _platform_compat.py module that wraps platform._sys_version() with
   a retry-on-ValueError fallback. This is more robust than cache-seeding
   because it handles all future platform._sys_version() calls, not just
   the first one.

2. Import the fix from both run.py and colab.py entrypoints, so Studio
   no longer crashes on Anaconda Python regardless of the launch path.

3. The wrapper is idempotent (guarded by a flag) and handles edge cases:
   paired pipes (Anaconda, conda-forge), unpaired pipes (hypothetical),
   and standard CPython strings (no-op since ValueError is never raised).

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

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

* Replace monkey-patch with cache-prime, fix colab.py duplicate sys.path, cover main.py

- Rewrite _platform_compat.py: replace function-wrapping monkey-patch with
  one-shot cache seed (_seed_sys_version_cache). Parses cleaned sys.version
  once and seeds platform._sys_version_cache so the stdlib parser never sees
  the problematic Anaconda/conda-forge pipe-delimited string. No function
  replacement, no idempotency flag, no reload edge cases.

- colab.py: remove duplicate backend_path sys.path insertion after
  _bootstrap_studio_venv(). The early insertion (before _platform_compat
  import) already covers it. This also fixes backend/ ending up behind
  venv site-packages in sys.path ordering.

- run.py: move PYTHONWARNINGS=ignore before _platform_compat import to
  preserve original intent of suppressing warnings early.

- main.py: add sys.path + _platform_compat import before route imports,
  covering the direct `uvicorn main:app` launch path.

- Add test_platform_compat.py with 7 tests covering Anaconda, conda-forge,
  and standard CPython version strings, plus the loggers import chain.

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

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

* Remove test_platform_compat.py from PR

* Handle Format B conda-forge version strings with duplicate paren groups

Some conda-forge builds produce sys.version with the build info both
before and after the pipe label (e.g. "3.9.7 (default, ...) | packaged
by conda-forge | (default, ...) \n[GCC 7.5.0]"). After stripping the
pipe segment, two consecutive (...) groups remain, which still fails
platform._sys_version(). Add a second regex pass to drop the duplicate
paren group.

* Guard _sys_version call with try/except to avoid making things worse

If the cleaned version string is still unparseable by the stdlib regex
(e.g. nested parens, exotic multi-pipe formats), silently give up
instead of letting ValueError propagate at import time -- which would
be a worse crash than the original deferred one.

---------

Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-22 05:36:55 -07:00
Manan Shah
6f129a214b
Fix Install commands for Windows + 1 line installs (#4447)
* One liner setup for unsloth studio

* Fix install scripts: system deps, activation bugs, curl/wget support

- install.sh: detect platform (macOS/Linux/WSL) and check for missing
  system dependencies (cmake, git, build-essential, libcurl4-openssl-dev).
  Prompt user once for permission to install all missing packages via
  brew (macOS) or sudo apt-get (Linux/WSL). Add wget fallback via
  download() helper since curl is not always present on minimal Linux
  installs. Fix nested curl|sh stdin stealing by downloading uv installer
  to a tempfile first. Replace venv activation (no-op in a pipe subshell)
  with explicit --python flag for uv pip install and direct venv binary
  invocation. Add idempotency guard for venv creation. Redirect stdin
  on unsloth studio setup to prevent pipe consumption. On macOS, check
  for Xcode Command Line Tools and trigger install if missing.

- install.ps1: wrap script body in Install-UnslothStudio function so
  that errors use return instead of exit (exit kills the terminal when
  run via irm|iex). Remove activate.ps1 invocation entirely -- use
  explicit --python path for uv pip install and & $UnslothExe for
  studio setup. This avoids both the child-scope activation bug (& vs
  dot-source) and the execution policy error on default Windows systems.
  Add winget availability check with clear error message. Fix PATH
  refresh to append registry paths instead of replacing the session PATH.
  Add uv installer fallback via astral.sh PowerShell script if winget
  install does not put uv on PATH. Broaden Python version check to
  accept 3.11-3.13. Add idempotency guard for venv creation.

- README.md: add wget one-liner alternative for systems without curl.

* Fix Tailwind CSS v4 .gitignore bug on Windows (#4444)

- Add .gitignore hiding workaround to setup.ps1 (matching existing
  setup.sh logic) so venv .gitignore files containing "*" don't prevent
  Tailwind's oxide scanner from finding .tsx source files
- Add CSS size validation to setup.sh, setup.ps1, and build.sh to catch
  truncated Tailwind builds early
- Remove stray force-rebuild overrides that made the "skip build if
  current" cache check dead code in both setup scripts
- Add rm -rf dist to build.sh to force clean rebuilds for wheel packaging

* Change default port 8000 to 8888, fix installer bugs, improve UX

- Change default Studio port from 8000 to 8888 across all entry points
  (run.py, studio.py, ui.py, colab.py, vite.config.ts, setup scripts)
- Update launch banner: "Launching with studio venv..." to
  "Launching Unsloth Studio... Please wait..."
- Add "Open your web browser" banner and rename labels
  (Local -> Local Access, External -> Worldwide Web Address)
- Fix venv idempotency: check for bin/python instead of just directory
  existence, clean up partial venvs on retry
- Fix build.sh CSS validation: handle empty CSS case that silently
  bypassed the check with "integer expression expected"
- Fix install.sh sudo handling: try apt-get without sudo first (works
  when root), then escalate with per-package tracking and user prompt
- Fix install.ps1: check exit code from studio setup, fail on error
- Add pciutils to WSL GGUF build dependencies
- Apply same smart apt-get escalation pattern to studio/setup.sh

* Use detected Python version for venv, abort on non-apt Linux

- install.ps1: detect existing Python 3.11/3.12/3.13 and use that
  version for venv creation instead of always forcing 3.13
- install.sh: exit with error on non-apt Linux distros when required
  packages cannot be auto-installed, instead of silently continuing

* Make sudo permission prompt more prominent with warning banner

* Add Accept [Y/n] sudo prompt to studio/setup.sh for consistency

* Fix native command exit code handling and sudo decline flow

install.ps1: Add $LASTEXITCODE checks after winget (Python), uv venv,
and uv pip install calls. $ErrorActionPreference only catches PowerShell
cmdlet errors, not native executable failures. The Python check also
handles winget returning non-zero for "already installed".

setup.sh: Skip llama-server build when user declines sudo or sudo is
unavailable. Previously the script continued to section 8 which would
fail with confusing errors (e.g. "gcc: command not found") since
build-essential was never installed.

* Move rm -rf llama.cpp inside build branch to preserve existing install

When _SKIP_GGUF_BUILD is set (user declined sudo or sudo unavailable),
the previous rm -rf would destroy an already-working llama-server before
the skip check ran. Move it inside the else branch so existing builds
are preserved when the rebuild is skipped.

---------

Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-19 02:09:09 -07:00
Daniel Han
1f12ba16df
Combine studio setup fixes: frontend caching, venv isolation, Windows CPU support (#4413)
* Allow Windows setup to complete without NVIDIA GPU

setup.ps1 previously hard-exited if nvidia-smi was not found, blocking
setup entirely on CPU-only or non-NVIDIA machines. The backend already
supports CPU and MLX (Apple Silicon) in chat-only GGUF mode, and the
Linux/Mac setup.sh handles missing GPUs gracefully.

Changes:
- Convert the GPU check from a hard exit to a warning
- Guard CUDA toolkit installation behind $HasNvidiaSmi
- Install CPU-only PyTorch when no GPU is detected
- Build llama.cpp without CUDA flags when no GPU is present
- Update doc comment to reflect CPU support

* Cache frontend build across setup runs

Skip the frontend npm install + build if frontend/dist already exists.
Previously setup.ps1 nuked node_modules and package-lock.json on every
run, and both scripts always rebuilt even when dist/ was already present.

On a git clone editable install, the first setup run still builds the
frontend as before. Subsequent runs skip it, saving several minutes.
To force a rebuild, delete frontend/dist and re-run setup.

* Show pip progress for PyTorch download on Windows

The torch CUDA wheel is ~2.8 GB and the CPU wheel is ~300 MB. With
| Out-Null suppressing all output, the install appeared completely
frozen with no feedback. Remove | Out-Null for the torch install
lines so pip's download progress bar is visible. Add a size hint
so users know the download is expected to take a while.

Also moves the Triton success message inside the GPU branch so it
only prints when Triton was actually installed.

* Guard CUDA env re-sanitization behind GPU check in llama.cpp build

The CUDA_PATH re-sanitization block (lines 1020-1033) references
$CudaToolkitRoot which is only set when $HasNvidiaSmi is true and
the CUDA Toolkit section runs. On CPU-only machines, $CudaToolkitRoot
is null, causing Split-Path to throw:

  Split-Path : Cannot bind argument to parameter 'Path' because it is null.

Wrap the entire block in `if ($HasNvidiaSmi -and $CudaToolkitRoot)`.

* Rebuild frontend when source files are newer than dist/

Instead of only checking if dist/ exists, compare source file timestamps
against the dist/ directory. If any file in frontend/src/ is newer than
dist/, trigger a rebuild. This handles the case where a developer pulls
new frontend changes and re-runs setup -- stale assets get rebuilt
automatically.

* Fix cmake not found on Windows after winget install

Two issues fixed:

1. After winget installs cmake, Refresh-Environment may not pick up the
   new PATH entry (MSI PATH changes sometimes need a new shell). Added a
   fallback that probes cmake's default install locations (Program Files,
   LocalAppData) and adds the directory to PATH explicitly if found.

2. If cmake is still unavailable when the llama.cpp build starts (e.g.
   winget failed silently or PATH was not updated), the build now skips
   gracefully with a [SKIP] warning instead of crashing with
   "cmake : The term 'cmake' is not recognized".

* Fix frontend rebuild detection and decouple oxc-validator install

Address review feedback:

- Check entire frontend/ directory for changes, not just src/.
  The build also depends on package.json, vite.config.ts,
  tailwind.config.ts, public/, and other config files. A change
  to any of these now triggers a rebuild.
- Move oxc-validator npm install outside the frontend build gate
  in setup.sh so it always runs on setup, matching setup.ps1
  which already had it outside the gate.

* Show cmake errors on failure and retry CUDA VS integration with elevation

Two fixes for issue #4405 (Windows setup fails at cmake configure):

1. cmake configure: capture output and display it on failure instead of
   piping to Out-Null. When the error mentions "No CUDA toolset found",
   print a hint about the CUDA VS integration files.

2. CUDA VS integration copy: when the direct Copy-Item fails (needs
   admin access to write to Program Files), retry with Start-Process
   -Verb RunAs to prompt for elevation. This is the root cause of the
   "No CUDA toolset found" cmake failure -- the .targets files that let
   MSBuild compile .cu files are missing from the VS BuildCustomizations
   directory.

* Address reviewer feedback: cmake PATH persistence, stale cache, torch error check

1. Persist cmake PATH to user registry so Refresh-Environment cannot
   drop it later in the same setup run. Previously the process-only
   PATH addition at phase 1 could vanish when Refresh-Environment
   rebuilt PATH from registry during phase 2/3 installs.

2. Clean stale CMake cache before configure. If a previous run built
   with CUDA and the user reruns without a GPU (or vice versa), the
   cached GGML_CUDA value would persist. Now the build dir is removed
   before configure.

3. Explicitly set -DGGML_CUDA=OFF for CPU-only builds instead of just
   omitting CUDA flags. This prevents cmake from auto-detecting a
   partial CUDA installation.

4. Fix CUDA cmake flag indentation -- was misaligned from the original
   PR, now consistently indented inside the if/else block.

5. Fail hard if pip install torch returns a non-zero exit code instead
   of silently continuing with a broken environment.

* Remove extra CUDA cmake flags to align Windows with Linux build

Drop GGML_CUDA_FA_ALL_QUANTS, GGML_CUDA_F16, GGML_CUDA_GRAPHS,
GGML_CUDA_FORCE_CUBLAS, and GGML_CUDA_PEER_MAX_BATCH_SIZE flags.
The Linux build in setup.sh only sets GGML_CUDA=ON and lets llama.cpp
use its defaults for everything else. Keep Windows consistent.

* Address reviewer round 2: GPU probe fallback, Triton check, stale binary rebuild

1. GPU detection: fallback to default nvidia-smi install locations
   (Program Files\NVIDIA Corporation\NVSMI, System32) when nvidia-smi
   is not on PATH. Prevents silent CPU-only provisioning on machines
   that have a GPU but a broken PATH.

2. Triton: check $LASTEXITCODE after pip install and print [WARN]
   on failure instead of unconditional [OK].

3. Stale llama-server: check CMakeCache.txt for GGML_CUDA setting
   and rebuild if the existing binary does not match the current GPU
   mode (e.g. CUDA binary on a now-CPU-only rerun, or vice versa).

* Fix frontend rebuild detection and npm dependency issues

Addresses reviewer feedback on the frontend caching logic:

1. setup.sh: Fix broken find command that caused exit under pipefail.
   The piped `find | xargs find -newer` had paths after the expression
   which GNU find rejects. Replaced with a simpler `find -maxdepth 1
   -type f -newer dist/` that checks ALL top-level files (catches
   index.html, bun.lock, etc. that the extension allowlist missed).

2. setup.sh: Guard oxc-validator npm install behind `command -v npm`
   check. When the frontend build is skipped (dist/ is cached), Node
   bootstrap is also skipped, so npm may not be available.

3. setup.ps1: Replace Get-ChildItem -Include with explicit path
   probing for src/ and public/. PowerShell's -Include without a
   trailing wildcard silently returns nothing, so src/public changes
   were never detected. Also check ALL top-level files instead of
   just .json/.ts/.js/.mjs extensions.

* Fix studio setup: venv isolation, centralized .venv_t5, uv targeting

- All platforms (including Colab) now create ~/.unsloth/studio/.venv
  with --without-pip fallback for broken ensurepip environments
- Add --python sys.executable to uv pip install in install_python_stack.py
  so uv targets the correct venv instead of system Python
- Centralize .venv_t5 bootstrap in transformers_version.py with proper
  validation (checks required packages exist, not just non-empty dir)
- Replace ~150 lines of duplicated install code across 3 worker files
  with calls to the shared _ensure_venv_t5_exists() helper
- Use uv-if-present with pip fallback; do not install uv at runtime
- Add site.addsitedir() shim in colab.py so notebook cells can import
  studio packages from the venv without system-Python double-install
- Update .venv_t5 packages: huggingface_hub 1.3.0->1.7.1, add hf_xet
- Bump transformers pin 4.57.1->4.57.6 in requirements + constraints
- Add Fast-Install helper to setup.ps1 with uv+pip fallback
- Keep Colab-specific completion banner in setup.sh

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

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

* Fix nvidia-smi PATH persistence and cmake requirement for CPU-only

1. Store nvidia-smi as an absolute path ($NvidiaSmiExe) on first
   detection. All later calls (Get-CudaComputeCapability,
   Get-PytorchCudaTag, CUDA toolkit detection) use this absolute
   path instead of relying on PATH. This survives Refresh-Environment
   which rebuilds PATH from the registry and drops process-only
   additions.

2. Make cmake fatal for CPU-only installs. CPU-only machines depend
   entirely on llama-server for GGUF chat mode, so reporting "Setup
   Complete!" without it is misleading. GPU machines can still skip
   the llama-server build since they have other inference paths.

* Fix broken frontend freshness detection in setup scripts

- setup.sh: Replace broken `find | xargs find -newer` pipeline with
  single `find ... -newer` call. The old pipeline produced "paths must
  precede expression" errors (silently suppressed by 2>/dev/null),
  causing top-level config changes to never trigger a rebuild.
- setup.sh: Add `command -v npm` guard to oxc-validator block so it
  does not fail when Node was not installed (build-skip path).
- setup.ps1: Replace `Get-ChildItem -Include` (unreliable without
  -Recurse on PS 5.1) with explicit directory paths for src/ and
  public/ scanning.
- Both: Add *.html to tracked file patterns so index.html (Vite
  entry point) changes trigger a rebuild.
- Both: Use -print -quit instead of piping to head -1 for efficiency.

* Fix bugs found during review of PRs #4404, #4400, #4399

- setup.sh: Add || true guard to find command that checks frontend/src
  and frontend/public dirs, preventing script abort under set -euo
  pipefail when either directory is missing

- colab.py: Use sys.path.insert(0, ...) instead of site.addsitedir()
  so Studio venv packages take priority over system copies. Add warning
  when venv is missing instead of silently failing.

- transformers_version.py: _venv_t5_is_valid() now checks installed
  package versions via .dist-info metadata, not just directory presence.
  Prevents false positives from stale or wrong-version packages.

- transformers_version.py: _install_to_venv_t5() now passes --upgrade
  so pip replaces existing stale packages in the target directory.

- setup.ps1: CPU-only PyTorch install uses --index-url for cpu wheel
  and all install commands use Fast-Install (uv with pip fallback).

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

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

* Fix _venv_t5_is_valid dist-info loop exiting after first directory

Remove premature break that caused the loop over .dist-info directories
to exit after the first match even if it had no METADATA file. Now
continues iterating until a valid METADATA is found or all dirs are
exhausted.

* Capture error output on failure instead of discarding with Out-Null

setup.ps1: 6 locations changed from `| Out-Null` to `| Out-String` with
output shown on failure -- PyTorch GPU/CPU install, Triton install,
venv_t5 package loop, cmake llama-server and llama-quantize builds.

transformers_version.py: clean stale .venv_t5 directory before reinstall
when validation detects missing or version-mismatched packages.

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

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

* Fix ModuleNotFoundError when CLI imports studio.backend.core

The backend uses bare "from utils.*" imports everywhere, relying on
backend/ being on sys.path. Workers and routes add it at startup, but
the CLI imports studio.backend.core as a package -- backend/ was never
added. Add sys.path setup at the top of core/__init__.py so lazy
imports resolve correctly regardless of entry point.

Fixes: unsloth inference unsloth/Qwen3-8B "who are you" crashing with
"No module named 'utils'"

* Fix frontend freshness check to detect all top-level file changes

The extension allowlist (*.json, *.ts, *.js, *.mjs, *.html) missed
files like bun.lock, so lockfile-only dependency changes could skip
the frontend rebuild. Check all top-level files instead.

* Add tiktoken to .venv_t5 for Qwen-family tokenizers

Qwen models use tiktoken-based tokenizers which fail when routed through
the transformers 5.x overlay without tiktoken installed. Add it to the
setup scripts (with deps for Windows) and runtime fallback list.

Integrates PR #4418.

* Fix tiktoken crash in _venv_t5_is_valid and stray brace in setup.ps1

_venv_t5_is_valid() crashed with ValueError on unpinned packages like
"tiktoken" (no ==version). Handle by splitting safely and skipping
version check for unpinned packages (existence check only).

Also remove stray closing brace in setup.ps1 tiktoken install block.

---------

Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-18 03:52:25 -07:00
Leo Borcherding
262271a20d
Fix/colab comment edits (#4317)
* Removing .precommit config

* edited colab comments

* studio: update Unsloth_Studio_Colab.ipynb

* studio: update Unsloth_Studio_Colab.ipynb

* studio: add Colab T4 GPU metadata to force T4 instance

* style: update colab popup to black/white theme with gem icon and play button

* feat: center landscape image in colab notebook

* style: shrink popup to fit content, truncate URL display

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

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

* feat: center landscape image in colab notebook

* feat: use GitHub raw URL for studio landscape image in notebook

* chore: update colab notebook

---------

Co-authored-by: LeoBorcherding <LeoBorcherding@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-16 16:15:46 -07:00
Roland Tannous
47654cb91c Final cleanup 2026-03-12 18:28:04 +00:00
Roland Tannous
a2baf80511 Update license headers 2026-03-12 17:23:10 +00:00
Roland Tannous
817f2e8dcc feat: integrate structlog, configure workers for prod logging, and migrate print statements 2026-03-11 12:33:16 +00:00
Roland Tannous
d882678fe4 Add AGPL-3.0 SPDX headers to all source files 2026-03-09 20:17:45 +00:00
Leo Borcherding
17df5bf3bf feat: Add simple 2-cell Colab notebook (no tunnel needed)
- Create studio/backend/colab.py using Colab's built-in proxy
- Uses google.colab.kernel.proxyPort() for URL (no cloudflare)
- Shows nice clickable link with IPython.display.HTML
- Notebook has just 2 cells: setup and start
- Much simpler than external tunneling approach
2026-02-17 04:57:30 -06:00