Commit graph

5,864 commits

Author SHA1 Message Date
pre-commit-ci[bot]
6618213da5 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-22 15:16:46 +00:00
danielhanchen
1e58c3707d Harden Gemma array parsing, XML-parameter guard, and stream teardown
Address five review findings on the Gemma tool-call and OpenAI passthrough
streaming paths:

- parse_tool_calls_from_text collected JSON and Gemma markers without the
  _inside_open_parameter guard, so a marker embedded in an existing
  <function=...><parameter=...> value was promoted to a separate tool call.
  Candidates that start inside an open XML parameter are now skipped, matching
  the guard the XML-style parser already applies.

- _quote_gemma_array_elements preserved array elements starting with { or [
  verbatim, so an array of objects (items:[{path:a}]) or a nested array failed
  json.loads and the whole call was dropped. Object and nested-array elements
  are now normalised recursively.

- _openai_passthrough_stream synthesized a finish chunk before a trailing
  usage-only chunk and set saw_finish_reason, which made the EOF guard skip the
  [DONE] sentinel. The EOF path now emits [DONE] whenever the upstream omitted
  it, even after a finish chunk was already synthesized.

- /generate/stream drove generation through asyncio.to_thread with no
  disconnect watcher, so a client disconnect during a long generation went
  unnoticed until the next send. It now runs _await_disconnect_then_cancel
  against the request, matching the other local streaming endpoints.

- _SameTaskStreamingResponse closed the body iterator with aclose() on a
  send-side disconnect, raising GeneratorExit so the generators' cancellation
  handlers (which finish the api_monitor entry) never ran. It now throws
  CancelledError, falling back to aclose() when athrow is unavailable.

Extends tests/test_gemma_tool_parse_edge_cases.py with array-of-objects,
nested-array, and marker-inside-XML-parameter cases.
2026-06-22 15:14:42 +00:00
danielhanchen
520df9fe9d Quote bare Gemma array elements; order finish before trailing usage
- _quote_gemma_object_keys skipped array values, so a Gemma call with a
  bare-string array argument like labels:[bug,ui] produced invalid JSON and
  the whole tool call was dropped. Array values are now scanned and bare
  string elements quoted, while numbers, quoted strings, and JSON literals
  are preserved.

- In the OpenAI passthrough stream, a trailing usage-only chunk
  (stream_options.include_usage) that arrived before any finish chunk was
  relayed before the synthetic finish, producing usage -> finish -> [DONE].
  Emit the synthetic finish before that usage chunk so the order matches the
  other streams (finish -> usage -> [DONE]).

Extends tests/test_gemma_tool_parse_edge_cases.py with the bare-array cases.
2026-06-22 12:59:00 +00:00
pre-commit-ci[bot]
11564256b9 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-22 12:26:05 +00:00
danielhanchen
eca61f7f5e Parse tool calls in document order and skip nested markers both ways
Unify the JSON- and Gemma-format tool-call passes into a single
position-ordered scan:

- Calls are now emitted in byte order across both formats, so a mixed
  output like `<|tool_call>call:create{...}<tool_call|> ... <tool_call>
  {"name":"read",...}</tool_call>` executes create before read, matching
  the order they appear in (tools run in returned order).

- A candidate that starts inside an already-accepted call's span is
  skipped, in both directions: a JSON marker inside a Gemma argument and a
  Gemma marker inside a JSON argument are treated as data, not promoted to
  a second executable tool call.

Extends tests/test_gemma_tool_parse_edge_cases.py with the ordering and
JSON-in-Gemma nesting cases.
2026-06-22 12:24:41 +00:00
danielhanchen
0083826dde Emit the terminal finish_reason chunk in GGUF streams
The OpenAI chat-completions GGUF tool stream and plain stream both built a
final ChatCompletionChunk carrying finish_reason but never yielded it, so
clients received the optional usage chunk and [DONE] with no chunk carrying
finish_reason. OpenAI-compatible consumers rely on that terminal choice to
distinguish stop/length/tool_calls. Yield it before the usage chunk and
[DONE], matching the other streaming paths.
2026-06-22 11:49:46 +00:00
danielhanchen
b3e244d658 Harden Gemma tool-call parsing and stream-error detection
Address three issues in the Gemma-native tool-call path:

- _quote_gemma_object_keys stopped a bare (unquoted) string value at the
  first comma, so an argument like `location:New York, NY` was split
  mid-value and the synthesized JSON failed to parse, dropping the whole
  tool call. A bare value now ends only at `}` or a comma that begins the
  next `key:` pair.

- parse_tool_calls_from_text scanned the entire response for Gemma markers
  even inside a tool call already parsed from a `<tool_call>{...}` JSON
  block, so a marker-like string inside an argument (data) was promoted to
  a second, unintended tool call. Matches inside an already-consumed call
  span are now skipped.

- _openai_passthrough_stream relied on _monitor_openai_sse_line to flag a
  stream error, which returns early when monitor_id is None
  (skip_api_monitor), so an upstream error chunk left saw_stream_error
  unset and the synthetic-finish guard emitted a successful finish_reason
  after a failed stream. Error chunks are now detected independently of API
  monitoring.

Adds tests/test_gemma_tool_parse_edge_cases.py covering the comma and
marker-injection cases.
2026-06-22 11:03:21 +00:00
pre-commit-ci[bot]
491586c556 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-22 04:59:32 +00:00
danielhanchen
21edbb94a3 Merge branch 'main' into fix-gemma4-openai-api-streams
Resolve studio/backend/routes/inference.py:
- keep the reasoning_content split (the #6471 fix) on the GGUF chat streams
- keep each streaming path's direct _SameTaskStreamingResponse return (the
  cancel-scope fix) and route main's new _sse_streaming_response helper through
  _SameTaskStreamingResponse too, so every SSE path stays in the request task
- keep both the new _SameTaskStreamingResponse class and main's
  _aclose_stream_resources teardown helper

All 466 tests across the 9 changed test files pass.
2026-06-22 04:58:22 +00:00
Daniel Han
9b5c94df32
CLI: stop unsloth connect from leaking Studio credentials to unverified servers (#6479)
* CLI: stop `unsloth connect` from leaking Studio credentials to unverified servers

`unsloth connect` (and `unsloth chat`) discovered a Studio base URL from
UNSLOTH_STUDIO_URL or the default localhost port after only an unauthenticated
/api/health probe, then sent credentials to it:

- keyless connect iterated every cached API key and sent each as a bearer token
  to {base}/v1/models, so a malicious or port-preempting endpoint could harvest
  all of them;
- with no cached key it self-issued a Studio JWT and POSTed it to
  {base}/api/auth/api-keys;
- unsloth chat sent the same self-issued JWT to the discovered base.

The key cache was a flat, global list with no binding to a server identity, so a
key minted for one Studio could be replayed to any other.

Changes:

- Scope the agent key cache per base URL so a key is only ever replayed to the
  exact server it was minted for. Pre-scoping flat caches are ignored rather
  than replayed (at most one extra local mint on the next launch).
- Gate every automatic credential flow to loopback bases. A non-loopback
  UNSLOTH_STUDIO_URL now requires an explicit --api-key and nothing is sent
  automatically. SSH-tunnelled Studios that land on 127.0.0.1 keep working.
- Mint the API key locally against the Studio auth DB instead of POSTing a
  self-issued JWT over the network, so no bearer token leaves the process on the
  local path.
- Apply the same loopback gate to connect_studio_server (used by unsloth chat).

Fully closing same-host loopback-port preemption needs a signed /api/health
handshake so the client can verify the server identity before sending anything;
that is tracked as a server-side follow-up.

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

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

* CLI: verify Studio server identity before auto-sending credentials

Adds a challenge-response so `unsloth connect` and `unsloth chat` can confirm a
discovered loopback endpoint is really this install's Studio (not a process
that preempted the port) before sending it a cached or freshly minted
credential. This closes the same-host loopback-preemption gap left open by the
previous commit, which could only limit the blast radius.

Server:

- storage.get_or_create_identity_secret(): a dedicated server-wide secret in
  app_secrets (kept separate from the per-user JWT secret), readable only by
  the same OS user.
- storage.compute_identity_proof(nonce) = HMAC-SHA256(identity secret, nonce).
- GET /api/auth/identity?nonce=<base64url>: unauthenticated, returns the proof.
  The nonce is opaque to the server and the proof reveals nothing about the
  secret, so answering is safe.

Client:

- verify_studio_identity(base): sends a fresh 32-byte nonce, recomputes the
  expected HMAC from the local same-user secret, and constant-time compares.
  Fails closed on any error.
- connect._agent_api_key gates the loopback cached-key replay and the local
  mint on it; connect_studio_server (used by unsloth chat) gates the
  self-issued JWT on it.

A server that cannot read this install's secret (a different OS user, or a
remote/fake endpoint) cannot produce a matching proof, so the client refuses
and falls back to an explicit --api-key.

Tests: studio/backend/tests/test_identity.py (proof determinism, secret
persistence and caching, route response and nonce validation) and additions to
test_connect.py (the verify gate refuses when unverified, an explicit key skips
the check, and an end-to-end client plus server handshake against a stub HTTP
server).

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

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

* CLI: mint through the verified server instead of the local auth DB

CodeQL (py/clear-text-storage-sensitive-data) flagged the API key written to
the per-server cache and the agent config files once it was sourced from
storage.create_api_key(); the original HTTP-minted key did not trip the query.

Now that the identity handshake cryptographically confirms the loopback
responder really is this Studio before anything is sent, minting through the
server's /api/auth/api-keys endpoint with a self-issued JWT is safe again and
restores the original, CodeQL-clean data flow. The local-DB mint path is
removed.

The security properties are unchanged: discovery is still loopback-gated and
identity-verified, the key cache is still scoped per server, and a credential
reaches the server only after the handshake has proven its identity. The only
difference from the previous commit is that the self-issued JWT is sent to the
already-verified loopback server rather than the key being minted in-process.

Tests updated to mint through the fake server again.

* CLI: address review feedback on connect credential handling

- Reuse a saved per-server key before the loopback/identity gate. Keys are
  scoped per base URL, so a key the user saved with --api-key for a remote or
  SSH-tunnelled Studio (whose identity secret the local handshake can't match)
  is replayed only to that exact server. The loopback + identity-handshake gate
  now guards just auto-minting (self-issuing a JWT and creating a new key),
  which is the path that needs a cryptographically verified local Studio. Fixes
  keyless reuse being impossible for remote/tunnelled Studios the user had
  saved a key for.

- connect_studio_server (unsloth chat / inference): when the user explicitly set
  UNSLOTH_STUDIO_URL but the server can't be safely attached (non-loopback, or
  identity unverifiable), fail with a clear message instead of silently loading
  the model locally. Opportunistic discovery of the local default still falls
  back to a local load.

- Harden cache parsing: tolerate a corrupt or hand-edited cache where a base
  maps to a non-list (which would otherwise iterate a string into
  single-character "keys"), and read the cache as UTF-8.

Tests updated and added: saved-key replay without the handshake for both local
and remote bases, keyless mint still refused when the loopback server is
unverified, and connect_studio_server erroring on an explicit remote while
falling back locally on default discovery.

* CLI: harden connect handshake against relay and gate cached minted keys

Addresses review feedback on the credential handshake:

- Refuse HTTP redirects on credential-bearing requests (the identity handshake,
  /v1/models, key minting, and the chat HTTP backend). A process squatting the
  discovered port could 302 /api/auth/identity to the real Studio and relay its
  valid proof, or bounce a bearer-token request to another base, and urllib
  follows redirects by default. A shared no-redirect opener now treats any 3xx
  as an error.

- Give cached keys provenance. Keys the user supplied with --api-key are "saved"
  and replay without the handshake (needed for remote or SSH-tunnelled Studios
  whose secret the local handshake can't match). Keys we auto-mint are "minted"
  and replay only after the identity handshake, so a port squatter can't collect
  a previously minted localhost key just by answering the health check. New cache
  shape: servers[base] = {"saved": [...], "minted": [...]}.

Known residual: a different-OS-user process that squats the port and can also
reach a genuine same-secret Studio elsewhere on loopback can still manually relay
the identity challenge. Fully closing that needs the proof bound to the server's
real listening port, or OS-level peer-credential checks; tracked as follow-up. A
same-user attacker is out of scope, since it can already read the 0600 key cache.

Tests: redirect rejection in the handshake, minted-cache requiring the handshake
while saved-cache bypasses it, and the existing suites updated for the new cache
shape. unsloth_cli (206) and test_identity.py (5) pass.

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

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

* CLI: keep urllib imports function-local in the no-redirect opener

The repo's import-hoist safety linter (scripts/verify_import_hoist.py) flags
hoisting urllib to module level because it re-points the 'urllib' name in the
pre-existing HttpChatBackend._request scope. Build the no-redirect opener lazily
with function-local urllib imports instead, matching this module's convention,
and restore the local 'import urllib.request' in _request and
verify_studio_identity.

* test(identity): skip route tests when routes.auth import chain is unavailable

The identity route tests build a TestClient from routes.auth, which pulls the
whole routes package (routes/__init__ -> inference -> llama_cpp, ...). In a
minimal test matrix without the heavy backend deps, or when another test in the
same process has already broken that import chain, importing it raised and the
two route tests hard-failed. Skip in that case instead: the proof crypto is
covered by the storage-level tests, and the full backend CI still exercises the
route. No behaviour change where the deps are present (5 passed in isolation).

* test(connect): make connect tests pass on native Windows

unsloth connect supports Windows: --no-launch prints PowerShell ($env:X =
"v" / Remove-Item Env:X) instead of POSIX (export/unset), and the launch
path bridges env into a Windows agent .exe over WSLENV. The tests hardcoded
the POSIX shell forms, so on a real windows-latest runner 12 of them failed on
the assertion string even though every command exited 0.

Add OS-aware assertion helpers (_assert_env_set / _assert_env_unset) that check
the right shell syntax for the host OS, and skip the two WSL-from-Linux shim
tests on native Windows (os.name is 'posix' inside WSL, so that path can't run
there). No change on Linux/macOS (57 passed); the connect command's behaviour
is untouched. Validated on a windows-latest staging runner.

* style(connect): tighten comments in the credential-leak fix

Condense the verbose explanatory comments and multi-line docstrings added by
this PR to one or two lines each, drop a few that just restated the code, and
keep the security rationale where it is load-bearing. Comment/whitespace only;
verified with unslothai/scripts comment_tools.py (check --strip-docstrings:
6/6 'code unchanged'). Tests unchanged: connect 57 passed, identity 5 passed.

* CLI/Studio: harden the identity handshake (review round)

Addresses the latest Codex/Gemini review of the handshake:

- Store the identity secret privately. sqlite3.connect created the auth DB
  world-readable under a 022 umask, so another OS user could read app_secrets
  and forge proofs, defeating the same-user assumption the handshake rests on.
  The auth dir and DB are now restricted to owner-only (0700/0600); the JWT
  secret and password hashes there get the same protection.

- Bind the proof to the server's listening port. The stateless HMAC(secret,
  nonce) was relayable: a process squatting the discovered port could proxy the
  challenge to the real Studio on another port and pass it back. The proof now
  covers the port the server actually listens on (from the socket, never the
  Host header) and the client checks it against the port it connected to, so a
  relayed proof from a different port no longer matches. Closes the manual-relay
  residual left after the redirect fix.

- Cap the identity response read (the server is still unverified at that point)
  and serve the identity route from a sync def so its first-call SQLite read
  runs in the threadpool instead of the event loop.

Tests: port-bound proof + relayed-proof rejection added; identity (5) and
unsloth_cli (58) suites pass. Verified end to end against a real backend
(auth DB owner-only, handshake + mint still succeed).

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

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

* CLI/Studio: bind the identity proof to the connection address, not just port

Follow-up to the port binding from the last review. Binding only to the port
left a cross-address relay: a squatter on a different loopback address but the
same port (for example localhost resolving to a squatter on ::1 while the real
Studio is on 127.0.0.1) could proxy the nonce to the real Studio and pass back
a proof that still matched, since both share the port.

The proof now covers the address and the port the connection landed on:

- Server: takes the address+port from request.scope, which uvicorn populates
  from getsockname, so it is the real local address the client reached even
  when Studio is bound to 0.0.0.0 (verified empirically), never the
  client-controlled Host header.

- Client: resolves the base host to one concrete IP, talks to exactly that IP,
  and binds the proof to (IP, port). A proof relayed from a Studio on a
  different address or port was computed for that other endpoint and no longer
  matches the one the client dialed.

Both sides normalise the address through ipaddress so equivalent forms compare
equal. Together with the private-secret and redirect fixes, this closes the
cross-user loopback relay an attacker can mount without reading the secret.

Tests: proof now bound to host+port; relayed-proof rejection retained; identity
(5) and unsloth_cli (58) suites pass. Verified end to end against a real backend
(handshake + mint still succeed).

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

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

* CLI: pick the loopback address at discovery so localhost does not regress

find_studio_server now resolves a bare localhost base to its concrete loopback
addresses and returns the first that answers /api/health, IPv4 127.0.0.1 first
(where unsloth studio binds by default). The whole flow (health probe, identity
check, credential send) then targets that one address instead of racing
IPv4/IPv6 resolution, where localhost could resolve ::1-first and hide a Studio
bound to 127.0.0.1. A literal IP or remote name is unchanged.

* [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-06-21 21:28:38 -07:00
Daniel Han
9f39cc2c39
Studio: use an isolated Node.js for the frontend build instead of replacing the system Node/npm (#6533)
* Studio: use an isolated Node.js for the frontend build instead of replacing the system Node/npm

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

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

* Studio: address Node isolation review (no-Node probe crash, PATH refresh, OXC provisioning, venv python, runtime node resolver)

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

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

* Fix/adjust Node isolation for PR #6533

* Studio Node: don't cache a negative node resolution; accept Node metadata in setup.sh ownership guard

- node_runtime: memoize only a version-adequate executable so a Node installed
  by a separate-process 'studio update' is picked up without a backend restart.
- setup.sh: _studio_owned_adoptable also accepts UNSLOTH_NODE_PREBUILT_INFO.json,
  matching the setup.ps1 Node ownership guard (custom-home parity).

* Studio setup.ps1: skip OXC npm install gracefully when npm is absent

Mirror setup.sh's `command -v npm` guard so a pip-installed Studio with no
system Node skips the OXC runtime install (validator degrades at runtime) instead
of exit 1 aborting the whole setup. Tighten test_node_probe_guard.ps1's probe
regex so it only matches the two system-version probes, not this new npm guard.

* Wire test_node_probe_guard.ps1 into Windows CI for PR #6533

* Harden isolated Node install and probes for PR #6533

- install_node_prebuilt.py: keep an existing, still-usable isolated Node
  when nodejs.org's dist index is unreachable instead of aborting the
  update on a transient outage (existing_install_usable + tolerant fetch).
- install_node_prebuilt.py: pin NPM_CONFIG_PREFIX/npm_config_prefix and
  drop NODE_PATH in _run_node so any npm -g stays inside the isolated
  prefix; Windows npm otherwise writes to %APPDATA%\npm.
- install_node_prebuilt.py: resolve tar hard-link targets against the
  archive root (symlink targets stay link-parent relative).
- setup.ps1: wrap the system node/npm probes in try/catch so a present
  but broken shim degrades to the bundled Node instead of aborting setup.
- setup.ps1: run the isolated Node install with the handed-off/venv Python
  (ReusedSetupPython); the main resolver runs later and bare python may be
  a Store stub this early.
- setup.sh: log when the OXC validator runtime is skipped for missing npm,
  matching setup.ps1.
- node_runtime.py: move the version-floor comment onto _version_meets_floor.
- Tests for the offline-reuse and broken-shim paths.

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

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

* Trim verbose comments across the Studio Node installer for PR #6533

Comments-only pass: collapse the multi-line section banners to single lines,
drop comments that restate obvious code, and tighten the remaining docstrings
and "why" notes without losing intent. No code changes (verified with an AST
comment-only check on the Python files and a non-comment-diff scan on setup.sh
and setup.ps1). Net 109 fewer lines; the install, decision, and probe-guard
suites stay green.

* Harden Node install from review: validated Python, version floor, legacy home, lock race

For PR #6533, addressing the latest review pass:

- setup.ps1: run the isolated Node install with the validated reused/venv Python.
  An incompatible reused interpreter (old venv, conda, stale UNSLOTH_SETUP_PYTHON)
  is no longer used; fall back to the resolved python instead.
- setup.ps1: a STUDIO_HOME/UNSLOTH_STUDIO_HOME override equal to the legacy default
  now uses the legacy sibling node dir (~/.unsloth/node), matching the runtime
  resolver and setup.sh, so OXC can find the Node it installed.
- install_node_prebuilt.py: reject an explicit --node-version below the floor
  (^20.19 || >=22.12 || >=23) instead of installing a Node the build cannot use.
- install_node_prebuilt.py: atomically rename a stale install lock before unlinking
  so two concurrent runs without filelock cannot both acquire it.

Tests added for the version floor (parametrized + explicit-below-floor rejection).
Full install suite: 937 passed, 1 skipped; setup.ps1 parses; decision tests green.

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

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

* Address latest review: armv7l + later-fetch offline reuse for PR #6533

- install_node_prebuilt.py: reject 32-bit ARM (armv7l) up front. Node 24 LTS
  ships no linux-armv7l build, so the old path failed late with a confusing
  "no sha256"; it now fails fast with a clear unsupported-architecture error.
- install_node_prebuilt.py: extend the offline-reuse fallback to the SHASUMS and
  archive fetches. If index.json resolves a newer Node but a later download fails
  and a usable isolated Node is already on disk, keep it instead of aborting a
  non-force update.

Tests added: armv7l/armhf are unsupported; a SHASUMS failure keeps an existing
usable Node and re-raises when none is present. Full install suite: 941 passed.

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

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

* Add UNSLOTH_STUDIO_HOME node-dir tests (install side + resolver) for PR #6533

* Add regression tests pinning the reuse path read-only and isolating installer writes

Lock in the two invariants behind the isolated-Node design: reusing a good
system Node never mutates the user's Node/npm, and the installer's own npm
calls only ever write inside its install_dir.

- tests/studio/install/test_install_node_prebuilt_logic.py: assert _run_node
  redirects NPM_CONFIG_PREFIX/npm_config_prefix into install_dir and drops an
  inherited NODE_PATH; assert _ensure_npm_floor scopes the npm self-upgrade to
  install_dir (never -g against the system) and is a no-op once npm meets the floor.
- tests/sh/test_system_node_readonly.sh (new, wired into studio-backend-ci.yml):
  the setup.sh NODE_SOURCE=system arm runs no global install and sets no
  NPM_CONFIG_PREFIX, with a positive control that the bundled arm does.
- tests/studio/test_node_decision.ps1: symmetric structural guard that the prefix
  pin and the only global install (bun) live in the bundled branch, not the system arm.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: wasimysaid <wasimysdev@gmail.com>
2026-06-21 21:17:29 -07:00
oobabooga
e6b4480832
Studio: simplify the inference backend (#6490) 2026-06-21 20:01:09 -03:00
Wasim Yousef Said
72254e0a81
Tighten comments for PR #6493 (#6539) 2026-06-21 05:49:07 -07:00
Daniel Han
01bc716708
Studio: fix llama.cpp update toast tag and reload hint (#6493)
* Studio: fix llama.cpp update toast tag and reload hint

The post-update toast used the job's to_tag, which is the bare bNNNN build
number (same as installed_tag), so it showed e.g. "b9726" instead of the full
release tag. Use status.latest_tag (e.g. b9726-mix-<sha>) to match the tag the
banner already shows, falling back to to_tag and then a generic label.

Also drop "Reload your model to use it." when there is nothing to reload: only
append it when a local model is loaded, since external-provider models do not
use llama.cpp.

* Fix/adjust llama update toast for PR #6493

---------

Co-authored-by: danielhanchen <michaelhan2050@gmail.com>
Co-authored-by: wasimysaid <wasimysdev@gmail.com>
2026-06-21 05:40:49 -07:00
Michael Han
4098b4977e
Studio: surface Apply for a chat-template-only edit so the override reloads (#6514)
Co-authored-by: Unsloth <michaelhan@Michaels-MacBook-Pro.local>
2026-06-21 02:26:36 -07:00
Michael Han
d8abd5b704
(feat) Add a setting to toggle the chat model disclaimer (#6513)
* Add a setting to toggle the chat model disclaimer

* Move disclaimer toggle into Chat menu section and refine description
2026-06-20 06:58:23 -07:00
Daniel Han
c53456adf7 Remove Windows Blackwell tips line from README 2026-06-20 12:09:44 +00:00
UmranPros
9e83399f9e
Studio: fix Gemma 4 separate-drafter MTP detection and fallback (#6459)
Recognise the Gemma 4 separate-drafter MTP family, auto-download the drafter with retry, fall back to n-gram with a clear reason when it cannot be resolved, and retry the download on reload. Gemma 3n (ships no drafter) and embedded-MTP models (Qwen) are unaffected.

Fixes #6406
2026-06-20 04:43:37 -07: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) 2026-06-20 01:23:57 -07:00
pre-commit-ci[bot]
4b68cac412 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-20 07:15:30 +00:00
Daniel Han
2b7360176b Gemma 4: parse bare-string tool args and keep safetensors tools for native <|tool_call>
- Quote bare unquoted string values in Gemma native tool-call args (e.g.
  {location:Tokyo,unit:celsius}) so they parse; JSON scalars stay typed.
- Stop _detect_safetensors_features from suppressing supports_tools for
  templates that emit Gemma native <|tool_call>, which the shared parser
  now reads.
- Add tests for both.
2026-06-20 07:14:44 +00: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
3a2c1c2b22
Merge branch 'main' into fix-gemma4-openai-api-streams 2026-06-20 00:00:27 -07:00
Lee Jackson
e1ae4756d9
Studio: rework prompt queue management UI/UX (#6467)
* feat: add prompt queue management UI

* fix: clean up prompt queue controls

* fix: address prompt queue review feedback

* fix: scope prompt queue controls to active thread

* fix: tighten prompt queue row behavior
2026-06-19 22:52:27 +01:00
pre-commit-ci[bot]
67ccd78440 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-19 20:49:41 +00:00
wasimysaid
db8d03927b Address stream cleanup and Gemma parser reviews 2026-06-19 22:49:06 +02:00
wasimysaid
6a57d3795a Avoid synthetic finish after passthrough errors 2026-06-19 21:54:50 +02:00
wasimysaid
3ca278cb79 Preserve audio stream disconnect cancellation 2026-06-19 21:16:29 +02:00
wasimysaid
5c4e7b5365 Fix tool parser compatibility exports lint 2026-06-19 20:48:10 +02:00
wasimysaid
27e0228fdf Fix Studio passthrough cold stream timeout 2026-06-19 20:40:29 +02:00
wasimysaid
5ba19c2977 Clean up Studio OpenAI stream helpers 2026-06-19 20:09:13 +02:00
wasimysaid
b53015be85 Address OpenAI stream review issues 2026-06-19 19:40:29 +02:00
wasimysaid
bd1e5eabe5 Harden OpenAI chat completion streams 2026-06-19 19:03:09 +02:00
wasimysaid
ede6a2bcee Avoid Responses stream task-group cleanup 2026-06-19 17:23:02 +02:00
wasimysaid
475ff786d8 Address Gemma stream review comments 2026-06-19 17:07:47 +02:00
wasimysaid
4c877cc7d8 Keep reasoning-only Responses output hidden 2026-06-19 16:51:40 +02:00
wasimysaid
6705053e9e Avoid duplicate Responses stream disconnect watcher 2026-06-19 16:33:32 +02:00
pre-commit-ci[bot]
05518f2769 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-19 14:29:10 +00:00
wasimysaid
165f4838e3 Fix Gemma 4 GGUF OpenAI API streams 2026-06-19 16:27:33 +02:00
oobabooga
7ce8dc73ac
Studio: label Apple Silicon as Metal/unified memory instead of CPU-only in installers (#6470)
* Studio: label Apple Silicon as Metal/unified memory instead of CPU-only in installers

* Studio: drop redundant aarch64 check from the macOS GPU label detection
2026-06-19 07:16:16 -07:00
Parvesh Saini
17e9714a98
studio: run /generate/stream's sync generator off the event loop to avoid blocking it (#6466)
* studio: run /generate/stream's sync generator off the event loop to avoid blocking it

* fix: close generator in finally on client disconnect in generate_stream

* Fix/adjust generate stream test for PR #6466

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

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

* Fix/adjust generate stream cancellation for PR #6466

* Fix/adjust generate stream cleanup for PR #6466

* fix: cancel incomplete generate stream cleanup

---------

Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: wasimysaid <wasimysdev@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: imagineer99 <samleejackson0@gmail.com>
2026-06-19 14:19:04 +01:00
Daniel Han
52877bba05
Studio: gate the MTP target-KV reserve to MTP spec mode, not just MLA (#6449)
_estimate_mtp_overhead_bytes is also reached for the separate-drafter spec modes
(draft-simple / draft-eagle3) through _user_draft_via_extras. Those modes load a
small distinct drafter with its own KV -- already counted in the draft KV +
weights -- and keep no duplicated full target context; only MTP runs a second
context over the target model's own KV geometry (llama.cpp ctx_tgt). Charging the
~main-KV-sized f16 copy there over-reserved by tens of GiB on an MLA model and
needlessly shrank the advertised context, the same under-advertising #6312 set
out to fix.

Thread mtp_keeps_target_ctx through _estimate_mtp_overhead_bytes (True for MTP,
False for separate-drafter modes) and derive _engaged_is_mtp at the fit call site
so the target copy is added only when the engaged mode is actually MTP. MLA + MTP
(GLM-5.2 / DeepSeek / Kimi) is unchanged, so the GLM-5.2 OOM fix is preserved;
non-MLA and the draft-simple / draft-eagle3 paths no longer pay the copy.

test_mtp_mla_target_ctx.py adds a case asserting the separate-drafter reserve
collapses to the draft KV (no target copy) while the default MTP path keeps it.
2026-06-19 05:51:35 -07:00
Daniel Han
76a2b9edf1
Studio: Auto disables MTP for MLA models (GLM-5.2 et al.); UNSLOTH_MLA_MTP_ENABLED to re-enable (#6468)
* Studio: Auto disables MTP for MLA models (GLM-5.2 et al.); UNSLOTH_MLA_MTP_ENABLED to re-enable

Studio's Auto speculative mode promotes any embedded-MTP model >=3B to
--spec-type draft-mtp. For MLA models (GLM-5.2/DeepSeek/Kimi) that is a
regression: llama.cpp's MLA/DSA MTP path keeps a duplicated full target-KV
context and recomputes the sparse-attention indexer every draft step, so it
runs ~2x slower than no speculation (GLM-5.2 UD-IQ1_S bench: 27 vs 45 tok/s,
flat across draft depth 1..6 and 96-100% acceptance, on both prose and code).
vLLM/SGLang get a speedup from the same model, so this is a llama.cpp
implementation gap, not a model property.

Auto now drops embedded MTP for MLA models and falls back to ngram-mod (or
spec-off when the binary lacks ngram-mod), mirroring the existing sub-3B
fallback. The metadata separator is kv_lora_rank: it is present on MLA models
and absent on non-MLA embedded-MTP models (Qwen3.x-MTP), whose MTP module is
structurally identical but fast, so a "full layer" heuristic cannot tell them
apart. Qwen MTP, separate drafters (Gemma, --model-draft), and non-MTP models
are unchanged.

Explicit overrides still engage the slower MTP route: choosing MTP / MTP+Ngram
in Settings, or passing --spec-type in extra args. UNSLOTH_MLA_MTP_ENABLED=1
re-enables Auto promotion for MLA once the upstream path is optimized.

A new spec_fallback_reason value "mla_mtp_disabled" surfaces this as an
Auto-mode policy downgrade (not a binary/update problem), with a settings
banner that points users at the MTP override. It is deliberately kept out of
the "Update llama.cpp" affordance since updating does not help.

Tests: resolver-matrix rows for MLA->ngram-mod / MLA-no-ngram->off /
non-MLA-Qwen->draft-mtp / MLA-separate-drafter->draft-mtp /
non-MTP-MLA->default / forced mtp|mtp+ngram on MLA->draft-mtp / env flag;
kv_lora_rank metadata fixtures; and reload-skip coverage (Auto ngram-mod is
idempotent, forced mtp bounces a reload).

* [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-06-19 05:40:16 -07:00
oobabooga
420799b61e
Studio: add an Open button to reveal the models folder in the file manager (#6452)
* Studio: add an Open button to reveal the models folder in the file manager

* Studio: report models folder creation failures

---------

Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com>
2026-06-19 05:14:58 -07:00
Lee Jackson
1eb15162d9
fix: clean up Studio warning log formatting (#6265)
* feat: queue chat prompts during generation

* fix: address prompt queue review edge cases

* fix: harden queued prompt dispatch

* fix: track queued prompt run state by thread

* fix: preserve prompt queue ordering

* fix: isolate prompt queue on new chat

* fix: clean up Studio warning log formatting

* Fix export log markup

---------

Co-authored-by: wasimysaid <wasimysdev@gmail.com>
2026-06-19 05:06:03 -07:00
Daniel Han
b552f2fbc8
studio: fix two backend CI test failures (capability dict + MTP recovery race) (#6464)
test_safetensors_capability_advertise: detect_reasoning_flags now returns a
reasoning_effort_levels key, so the none-template expectation must include it.

test_tensor_parallel::test_runtime_recovery_reloads_without_mtp: the assertion
raced the recovery thread, which sets _spec_fallback_reason just before its
finally clears _mtp_runtime_fallback_in_progress. Wait for the flag to clear
before asserting.

Co-authored-by: danielhanchen <michaelhan2050@gmail.com>
2026-06-19 04:33:52 -07:00
Lee Jackson
66e4568c2c
Unify retrieval slider styling (#6436) 2026-06-19 12:07:10 +02:00
Michael Han
aec27263e7
Polish the Hub card carousel: edge shadows, fades, and drag-to-scroll (#6461)
* Fix clipped card shadows in Hub trending carousel

The carousel scroller only had vertical padding, so with overflow-x set
the first and last cards had their drop shadow clipped on the horizontal
edges. Add px-2 with a matching -mx-2 so the shadow has room while the
cards stay aligned with the section heading, and scroll-px-2 so snap-start
does not scroll the padding away on load.

* Align carousel edge fades with the scroll clip edge

The shadow fix gave the scroller an -mx-2 bleed, but the left/right fade
overlays stayed pinned to the wrapper edges, 8px inside the clip edge. That
left a thin strip where a card showed beside the fade, so the fade read as a
separate block instead of blending into the background. Offset both fades by
the same 8px so their opaque edge sits on the clip edge.

* Add click-and-drag panning to the Hub card carousel

The rows only scrolled by wheel or trackpad, and grabbing a card started
a native drag of its avatar image, so the cards could not be dragged to
move the row. Add mouse drag-to-scroll (touch and pen keep native
scrolling), swallow the click a drag would otherwise fire on a card, keep
plain clicks working, and block the avatar's native drag.

* Trim carousel edge fade width from 56px to 44px

* Smooth out carousel drag panning

Scroll snap was correcting the position on every drag frame, which made
the pan feel sticky. Disable snap while a drag is active and restore it on
release so the row follows the pointer and then settles on a card.

* Drop stale carousel drag when the button is released off-element

If a press ended outside the scroller before the drag threshold was
crossed, no pointerup reached us and the drag stayed armed, so a later
buttonless mousemove would scroll the row. Bail out and clear the drag
whenever the primary button is no longer held.
2026-06-19 01:54:33 -07:00
oobabooga
45f060899e
studio/frontend: align custom titlebar sidebar surface with the real sidebar (#6455) 2026-06-18 20:05:02 -03:00