From 382683ebdc0b7ed55f53cd7698664164be843dab Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 22 May 2026 09:31:28 -0700 Subject: [PATCH 1/6] Bump pinned PyPI floor to unsloth>=2026.5.6 (#5716) PyPI release 2026.5.6 is now live; update install.sh and install.ps1 to pin against the new minimum so fresh installs pick up the latest wheel. Co-authored-by: Michael Han --- install.ps1 | 10 +++++----- install.sh | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/install.ps1 b/install.ps1 index 012d22608a..0dad877645 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1300,7 +1300,7 @@ shell.Run cmd, 0, False if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.5.5" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.5.6" unsloth-zoo } if ($baseInstallExit -eq 0) { $NoTorchReq = Find-NoTorchRuntimeFile if ($NoTorchReq) { @@ -1308,7 +1308,7 @@ shell.Run cmd, 0, False } } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.5.5" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.5.6" unsloth-zoo } } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red @@ -1346,7 +1346,7 @@ shell.Run cmd, 0, False if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.5.5" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.5.6" unsloth-zoo } if ($baseInstallExit -eq 0) { $NoTorchReq = Find-NoTorchRuntimeFile if ($NoTorchReq) { @@ -1354,7 +1354,7 @@ shell.Run cmd, 0, False } } } elseif ($StudioLocalInstall) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.5.5" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.5.6" unsloth-zoo } } else { $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth -- "$PackageName" } } @@ -1382,7 +1382,7 @@ shell.Run cmd, 0, False Write-TauriLog "STEP" "Installing unsloth" substep "installing unsloth (this may take a few minutes)..." if ($StudioLocalInstall) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.5.5" --torch-backend=auto } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.5.6" --torch-backend=auto } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) diff --git a/install.sh b/install.sh index 29acb5e190..384e9640d0 100755 --- a/install.sh +++ b/install.sh @@ -1865,7 +1865,7 @@ if [ "$_MIGRATED" = true ]; then # to prevent transitive torch resolution. run_install_cmd "install unsloth (migrated no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ - "unsloth>=2026.5.5" unsloth-zoo + "unsloth>=2026.5.6" unsloth-zoo _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" @@ -1873,7 +1873,7 @@ if [ "$_MIGRATED" = true ]; then else run_install_cmd "install unsloth (migrated)" uv pip install --python "$_VENV_PY" \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ - "unsloth>=2026.5.5" unsloth-zoo + "unsloth>=2026.5.6" unsloth-zoo fi if [ "$STUDIO_LOCAL_INSTALL" = true ]; then substep "overlaying local repo (editable)..." @@ -2041,7 +2041,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. run_install_cmd "install unsloth (no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --upgrade-package unsloth --upgrade-package unsloth-zoo \ - "unsloth>=2026.5.5" unsloth-zoo + "unsloth>=2026.5.6" unsloth-zoo _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" @@ -2056,7 +2056,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then fi elif [ "$STUDIO_LOCAL_INSTALL" = true ]; then run_install_cmd "install unsloth (local)" uv pip install --python "$_VENV_PY" \ - --upgrade-package unsloth "unsloth>=2026.5.5" unsloth-zoo + --upgrade-package unsloth "unsloth>=2026.5.6" unsloth-zoo substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps substep "overlaying unsloth-zoo from git main..." @@ -2088,7 +2088,7 @@ else tauri_log "STEP" "Installing Unsloth" substep "installing unsloth (this may take a few minutes)..." if [ "$STUDIO_LOCAL_INSTALL" = true ]; then - run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.5.5" --torch-backend=auto + run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.5.6" --torch-backend=auto substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps substep "overlaying unsloth-zoo from git main..." From 83b20976f747533c0d92a734c102bbe5d5af81cf Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 23 May 2026 06:59:16 -0700 Subject: [PATCH 2/6] ci: unblock Studio Windows + Linux + Mac smoke (#5741) Bundles three independent CI regressions hitting the maintainer PR backlog. Each one is verified end-to-end on a staging fork against real Ubuntu / macOS / Windows GitHub-hosted runners before this lands. 1. Windows --no-torch install: pydantic + pydantic-core drift to incompatible versions under `uv pip install --no-deps -r no-torch-runtime.txt` because pip resolves each independently from latest. pydantic.VERSION 2.13.4 pins pydantic-core==2.46.4 but pydantic-core 2.47.0 was the freshest published wheel, so `import pydantic` raised `SystemError: pydantic-core 2.47.0 is incompatible with the current pydantic version`. Resolve pydantic WITH deps in a focused pip call (install.sh, install.ps1, install_python_stack.py) before the --no-deps no-torch-runtime pass so pip pins pydantic-core to the version pydantic declares. pydantic's transitive deps (annotated-types, pydantic-core, typing-extensions, typing-inspection) are torch-free. Drop the redundant `Patch Studio venv with full typer / pydantic dep trees` workaround from the four Windows smoke YAMLs. Supersedes #5733 + #5734. 2. Linux Studio Update CI: upstream llama.cpp b9261+ split each binary's entry code into a paired `libllama--impl.so` shared library. `llama-server` and `llama-quantize` NEEDED-link against `libllama-server-impl.so` / `libllama-quantize-impl.so` with RUNPATH `$ORIGIN`, so the prebuilt overlay must copy those alongside the binaries. Without that, ldd reports them missing, preflight rejects, the installer falls back to source build, and studio-update-smoke annotates `setup.sh idempotency regressed`. Add `libllama-*-impl.so*` to the Linux runtime patterns and lock the pattern in test_rocm_support.TestRuntimePatterns. 3. Mac Studio UI Chat: change-password submit clicked while disabled. The disable gate only checked new + confirm password length, but Playwright's first click landed before the current-password field's React state had committed, so the form was simultaneously logically-invalid (current_password empty) and the button was disabled. Tighten the gate to require `currentPassword.length >= 8` and mirror the same check in the submit handler so Enter / autofill cannot bypass. Supersedes #5738. --- .../workflows/studio-windows-api-smoke.yml | 14 ------- .../studio-windows-inference-smoke.yml | 42 ------------------- .github/workflows/studio-windows-ui-smoke.yml | 14 ------- .../workflows/studio-windows-update-smoke.yml | 26 ------------ install.ps1 | 15 +++++++ install.sh | 13 ++++++ .../backend/requirements/no-torch-runtime.txt | 21 ++++++---- .../features/auth/components/auth-form.tsx | 14 +++++-- studio/install_llama_prebuilt.py | 13 ++++++ studio/install_python_stack.py | 21 ++++++++++ tests/studio/install/test_rocm_support.py | 8 ++++ 11 files changed, 94 insertions(+), 107 deletions(-) diff --git a/.github/workflows/studio-windows-api-smoke.yml b/.github/workflows/studio-windows-api-smoke.yml index 1d12ea6f90..eee61516c7 100644 --- a/.github/workflows/studio-windows-api-smoke.yml +++ b/.github/workflows/studio-windows-api-smoke.yml @@ -170,20 +170,6 @@ jobs: fi cygpath -w "$SHIM_DIR" >> "$GITHUB_PATH" - - name: Patch Studio venv with full typer / pydantic dep trees - # Belt-and-suspenders: install.ps1's --no-deps install of - # no-torch-runtime.txt drops typer's and pydantic's runtime - # deps unless explicitly pinned. Re-install the ones whose - # deps don't pull torch. - run: | - STUDIO_PY=~/.unsloth/studio/unsloth_studio/Scripts/python.exe - if [ ! -f "$STUDIO_PY" ]; then - echo "::error::Studio venv python not at $STUDIO_PY" - ls -la ~/.unsloth/studio/ || true - exit 1 - fi - "$STUDIO_PY" -m pip install --upgrade typer pydantic huggingface_hub - - name: Install pyjwt for the JWT-expiry forge test run: python -m pip install 'pyjwt>=2.6' diff --git a/.github/workflows/studio-windows-inference-smoke.yml b/.github/workflows/studio-windows-inference-smoke.yml index ad739dd529..6253b9d213 100644 --- a/.github/workflows/studio-windows-inference-smoke.yml +++ b/.github/workflows/studio-windows-inference-smoke.yml @@ -190,20 +190,6 @@ jobs: fi cygpath -w "$SHIM_DIR" >> "$GITHUB_PATH" - - name: Patch Studio venv with full typer / pydantic dep trees - # Belt-and-suspenders: install.ps1's --no-deps install of - # no-torch-runtime.txt drops typer's and pydantic's runtime - # deps unless explicitly pinned. Re-install the ones whose - # deps don't pull torch. - run: | - STUDIO_PY=~/.unsloth/studio/unsloth_studio/Scripts/python.exe - if [ ! -f "$STUDIO_PY" ]; then - echo "::error::Studio venv python not at $STUDIO_PY" - ls -la ~/.unsloth/studio/ || true - exit 1 - fi - "$STUDIO_PY" -m pip install --upgrade typer pydantic huggingface_hub - - name: Install OpenAI + Anthropic Python SDKs run: python -m pip install 'openai>=1.50' 'anthropic>=0.40' @@ -543,20 +529,6 @@ jobs: fi cygpath -w "$SHIM_DIR" >> "$GITHUB_PATH" - - name: Patch Studio venv with full typer / pydantic dep trees - # Belt-and-suspenders: install.ps1's --no-deps install of - # no-torch-runtime.txt drops typer's and pydantic's runtime - # deps unless explicitly pinned. Re-install the ones whose - # deps don't pull torch. - run: | - STUDIO_PY=~/.unsloth/studio/unsloth_studio/Scripts/python.exe - if [ ! -f "$STUDIO_PY" ]; then - echo "::error::Studio venv python not at $STUDIO_PY" - ls -la ~/.unsloth/studio/ || true - exit 1 - fi - "$STUDIO_PY" -m pip install --upgrade typer pydantic huggingface_hub - - name: Reset auth + boot Studio (API-only, default tool policy) run: | unsloth studio reset-password @@ -982,20 +954,6 @@ jobs: fi cygpath -w "$SHIM_DIR" >> "$GITHUB_PATH" - - name: Patch Studio venv with full typer / pydantic dep trees - # Belt-and-suspenders: install.ps1's --no-deps install of - # no-torch-runtime.txt drops typer's and pydantic's runtime - # deps unless explicitly pinned. Re-install the ones whose - # deps don't pull torch. - run: | - STUDIO_PY=~/.unsloth/studio/unsloth_studio/Scripts/python.exe - if [ ! -f "$STUDIO_PY" ]; then - echo "::error::Studio venv python not at $STUDIO_PY" - ls -la ~/.unsloth/studio/ || true - exit 1 - fi - "$STUDIO_PY" -m pip install --upgrade typer pydantic huggingface_hub - - name: Install OpenAI + Anthropic Python SDKs run: python -m pip install 'openai>=1.50' 'anthropic>=0.40' diff --git a/.github/workflows/studio-windows-ui-smoke.yml b/.github/workflows/studio-windows-ui-smoke.yml index e5ab9f8ab7..e3c8642122 100644 --- a/.github/workflows/studio-windows-ui-smoke.yml +++ b/.github/workflows/studio-windows-ui-smoke.yml @@ -208,20 +208,6 @@ jobs: cygpath -w "$SHIM_DIR" >> "$GITHUB_PATH" echo "Added Studio shim dir to PATH: $(cygpath -w "$SHIM_DIR")" - - name: Patch Studio venv with full typer / pydantic dep trees - # Belt-and-suspenders: install.ps1's --no-deps install of - # no-torch-runtime.txt drops typer's and pydantic's runtime - # deps unless explicitly pinned. Re-install the ones whose - # deps don't pull torch. - run: | - STUDIO_PY=~/.unsloth/studio/unsloth_studio/Scripts/python.exe - if [ ! -f "$STUDIO_PY" ]; then - echo "::error::Studio venv python not at $STUDIO_PY" - ls -la ~/.unsloth/studio/ || true - exit 1 - fi - "$STUDIO_PY" -m pip install --upgrade typer pydantic huggingface_hub - - name: Install Playwright + Chromium # No --with-deps on Windows: that flag installs Linux apt # packages. windows-latest ships the system frameworks diff --git a/.github/workflows/studio-windows-update-smoke.yml b/.github/workflows/studio-windows-update-smoke.yml index b477b3fa11..aa3e35f052 100644 --- a/.github/workflows/studio-windows-update-smoke.yml +++ b/.github/workflows/studio-windows-update-smoke.yml @@ -176,32 +176,6 @@ jobs: fi cygpath -w "$SHIM_DIR" >> "$GITHUB_PATH" - - name: Patch Studio venv with full typer / pydantic dep trees - # install.ps1 runs `uv pip install --no-deps -r - # no-torch-runtime.txt` to keep torch out of transitive - # resolution from accelerate/peft/trl. That also drops - # typer's and pydantic's runtime deps unless they're - # explicitly pinned in no-torch-runtime.txt. We pin the - # known ones (click, shellingham, annotated-doc, rich, - # pydantic-core, annotated-types, typing-inspection, ...) - # but typer / pydantic minor versions can introduce new - # transitive deps that are NOT in our pin list. - # - # Belt-and-suspenders: re-install typer + pydantic + - # huggingface_hub WITH their deps into the Studio venv. - # `pip install --upgrade` only adds missing packages; it - # never down-shifts an installed version. Cannot pull - # torch (none of typer / pydantic / huggingface_hub depend - # on it). - run: | - STUDIO_PY=~/.unsloth/studio/unsloth_studio/Scripts/python.exe - if [ ! -f "$STUDIO_PY" ]; then - echo "::error::Studio venv python not at $STUDIO_PY" - ls -la ~/.unsloth/studio/ || true - exit 1 - fi - "$STUDIO_PY" -m pip install --upgrade typer pydantic huggingface_hub - - name: First update should be a no-op (prebuilt already validated) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/install.ps1 b/install.ps1 index 0dad877645..b26566cc3d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1301,6 +1301,16 @@ shell.Run cmd, 0, False # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.5.6" unsloth-zoo } + if ($baseInstallExit -eq 0) { + # Install pydantic WITH deps so pip pins pydantic-core to + # the exact version pydantic's metadata requires. The + # --no-deps install of no-torch-runtime.txt below would + # otherwise pick the latest of each independently and + # trip pydantic's _ensure_pydantic_core_version check. + # pydantic's deps (annotated-types, pydantic-core, + # typing-extensions, typing-inspection) are torch-free. + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython pydantic } + } if ($baseInstallExit -eq 0) { $NoTorchReq = Find-NoTorchRuntimeFile if ($NoTorchReq) { @@ -1347,6 +1357,11 @@ shell.Run cmd, 0, False # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.5.6" unsloth-zoo } + if ($baseInstallExit -eq 0) { + # Install pydantic WITH deps so pip pins pydantic-core to + # the matching version (see migrated branch above). + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython pydantic } + } if ($baseInstallExit -eq 0) { $NoTorchReq = Find-NoTorchRuntimeFile if ($NoTorchReq) { diff --git a/install.sh b/install.sh index 384e9640d0..9bdd935171 100755 --- a/install.sh +++ b/install.sh @@ -1866,6 +1866,15 @@ if [ "$_MIGRATED" = true ]; then run_install_cmd "install unsloth (migrated no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ "unsloth>=2026.5.6" unsloth-zoo + # Install pydantic WITH deps so pip pins pydantic-core to the + # exact version pydantic's own metadata requires. The --no-deps + # install below would otherwise pick the latest of each + # independently and trip pydantic's _ensure_pydantic_core_version + # check on the next import. pydantic's deps (annotated-types, + # pydantic-core, typing-extensions, typing-inspection) are + # torch-free, so this is safe on the no-torch path. + run_install_cmd "install pydantic (with deps for compatible core)" \ + uv pip install --python "$_VENV_PY" pydantic _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" @@ -2042,6 +2051,10 @@ elif [ -n "$TORCH_INDEX_URL" ]; then run_install_cmd "install unsloth (no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --upgrade-package unsloth --upgrade-package unsloth-zoo \ "unsloth>=2026.5.6" unsloth-zoo + # Install pydantic WITH deps so pip pins pydantic-core to the + # exact version pydantic requires (see migrated branch above). + run_install_cmd "install pydantic (with deps for compatible core)" \ + uv pip install --python "$_VENV_PY" pydantic _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" diff --git a/studio/backend/requirements/no-torch-runtime.txt b/studio/backend/requirements/no-torch-runtime.txt index a39666495e..c33ebf4d94 100644 --- a/studio/backend/requirements/no-torch-runtime.txt +++ b/studio/backend/requirements/no-torch-runtime.txt @@ -22,14 +22,19 @@ rich>=13.0 markdown-it-py>=3.0 mdurl>=0.1 pygments>=2.0 -pydantic -# pydantic 2.x deps. With --no-deps, `import pydantic` blows up -# with `ModuleNotFoundError: 'pydantic_core'` (compiled Rust core, -# separate wheel), then `'annotated_types'`, then -# `'typing_inspection'` (used by pydantic 2.10+ for fields). -pydantic-core -annotated-types>=0.6 -typing-inspection>=0.4 +# pydantic is intentionally NOT installed via this --no-deps file. +# install.sh / install.ps1 / install_python_stack.py run a separate +# `pip install pydantic` (with deps) just before this file is +# applied, so pip resolves `pydantic-core` to the exact version +# pydantic's `_ensure_pydantic_core_version` check expects. Listing +# pydantic + pydantic-core unpinned here and resolving them under +# --no-deps used to pick the latest of each independently and trip +# `SystemError: pydantic-core 2.X.Y is incompatible with the current +# pydantic version` on the first import (Windows fresh-venv repro +# was the canonical case). pydantic's transitive deps +# (annotated-types, pydantic-core, typing-extensions, +# typing-inspection) are torch-free, so installing it WITH deps +# does not pull torch. pyyaml nest-asyncio diff --git a/studio/frontend/src/features/auth/components/auth-form.tsx b/studio/frontend/src/features/auth/components/auth-form.tsx index 3ac97929d8..d753e1aab5 100644 --- a/studio/frontend/src/features/auth/components/auth-form.tsx +++ b/studio/frontend/src/features/auth/components/auth-form.tsx @@ -194,7 +194,10 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { const hasBootstrapPassword = Boolean(window.__UNSLOTH_BOOTSTRAP__?.password); const invalidChangePasswordForm = !isLoginMode && - (newPassword.length < 8 || newPassword !== confirmPassword || currentPassword === newPassword); + (currentPassword.length < 8 || + newPassword.length < 8 || + newPassword !== confirmPassword || + currentPassword === newPassword); const showPasswordMismatchWarning = !isLoginMode && newPassword.length > 0 && @@ -206,8 +209,13 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { setError(null); if (!isLoginMode) { - if (!currentPassword) { - setError("Unable to initialize setup. Reload the page and try again."); + // Mirror the disable gate: Enter / autofill can bypass the button. + if (currentPassword.length < 8) { + setError( + currentPassword + ? "Current password must be at least 8 characters." + : "Unable to initialize setup. Reload the page and try again.", + ); return; } if (newPassword.length < 8) { diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index dd44f19691..91076a4743 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -3833,6 +3833,19 @@ def runtime_patterns_for_choice(choice: AssetChoice) -> list[str]: "llama-quantize", "libllama-common.so*", "libllama.so*", + # Upstream llama.cpp split the per-binary entry code into + # paired ``libllama--impl.so`` shared libraries + # around release b9261. ``llama-server`` and + # ``llama-quantize`` are NEEDED-linked against + # ``libllama-server-impl.so`` / ``libllama-quantize-impl.so`` + # respectively, with RUNPATH ``$ORIGIN``. Without copying + # the impl ``.so`` files alongside the binaries, ldd + # reports them missing, preflight rejects the install, and + # the installer falls back to a source build on a fresh + # Linux install. Glob the whole family so future bundles + # that split additional binaries (e.g. ``llama-cli``, + # ``llama-bench``) keep working. + "libllama-*-impl.so*", "libggml.so*", "libggml-base.so*", "libmtmd.so*", diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index ab234ad566..4dfa20032b 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -979,6 +979,27 @@ def install_python_stack() -> int: package_name, "unsloth-zoo", ) + # Pydantic ships its core as a separate compiled wheel + # (pydantic-core), and pydantic's ``_ensure_pydantic_core_version`` + # checks the installed core matches the exact version pinned in + # its own metadata. With ``--no-deps`` plus an unpinned + # ``pydantic`` / ``pydantic-core`` pair in no-torch-runtime.txt, + # pip resolved each to the newest available version and the two + # drifted (pydantic 2.13.4 pins pydantic-core==2.46.4 today, but + # pydantic-core 2.47.0 was the latest). On a fresh Windows venv + # the next ``import pydantic`` raised ``SystemError: ... + # incompatible with the current pydantic version``. + # + # Resolve them WITH deps in a focused pip call so pip picks a + # compatible pair. pydantic's own deps are + # ``annotated-types``, ``pydantic-core``, ``typing-extensions``, + # ``typing-inspection`` -- none of which transitively pull + # torch, so this is safe for the no-torch path. + pip_install( + "Installing pydantic (with deps for compatible core)", + "--no-cache-dir", + "pydantic", + ) pip_install( "Installing no-torch runtime deps", "--no-cache-dir", diff --git a/tests/studio/install/test_rocm_support.py b/tests/studio/install/test_rocm_support.py index 13df049c2d..698625e59b 100644 --- a/tests/studio/install/test_rocm_support.py +++ b/tests/studio/install/test_rocm_support.py @@ -346,6 +346,14 @@ class TestRuntimePatterns: patterns = runtime_patterns_for_choice(choice) assert "llama-server" in patterns assert "llama-quantize" in patterns + # Upstream split entry code into ``libllama--impl.so`` + # shared libraries (b9261+). llama-server and llama-quantize + # are NEEDED-linked against ``libllama-server-impl.so`` and + # ``libllama-quantize-impl.so`` respectively with RUNPATH + # ``$ORIGIN``, so the prebuilt overlay MUST copy them + # alongside the binaries or ldd reports them missing and + # preflight forces a source-build fallback. + assert "libllama-*-impl.so*" in patterns def test_linux_cuda_patterns(self): choice = AssetChoice( From dfb3eedf77d1e04ce68a5b2de0aa9f7a3c81acc0 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 23 May 2026 21:48:12 -0700 Subject: [PATCH 3/6] ci: broaden Linux + narrow Windows llama.cpp runtime patterns + trim #5741 comments (#5746) * ci: broaden Linux llama.cpp runtime pattern to lib*.so* #5741 patched the explicit Linux pattern list to add ``libllama-*-impl.so*`` after ggml-org/llama.cpp#23462 (between b9279 and b9283) split each binary's entry code into a paired ``lib-impl.so`` shared library. Same class of upstream repackaging will hit us again whenever a new shared lib is added. Mirror what macOS already does and replace the per-lib list with a single ``lib*.so*`` glob. ``copy_globs`` (line 3614) unions patterns, so the per-variant ``libggml-cuda.so*`` / ``libggml-hip.so*`` entries were never filtering anything; the spec lives in ``runtime_payload_health_groups`` (line 5209) which keeps the explicit minimum-required list per variant. Dry-run against b9296-bin-ubuntu-x64.tar.gz: 40 files copied (all ggml, llama, mtmd, impl variants + the two binaries we ship), 22 skipped (other CLIs, rpc-server, LICENSE). Functionally equal to the post-#5741 set. * cleanup: trim #5741 comments on the pydantic split Comments added in #5741 explained the original bug in full each time. They are mostly redundant with the commit message and the PR. Trim them to one short paragraph per site. No behavior change. * ci: narrow Windows runtime pattern to llama-server.exe + llama-quantize.exe Studio only invokes llama-server and llama-quantize. Mac and Linux already filter to those two binaries; Windows was the odd one out with ``*.exe`` copying every CLI upstream ships (llama-cli, llama-bench, llama-mtmd-cli, ...). Dry-run on b9296 (win cpu-x64, cpu-arm64, cuda-13.1, hip-radeon): 20 unused EXEs skipped per variant, all DLLs (incl. the new llama-*-impl.dll family) still copied via ``*.dll``. ``existing_install_matches_choice`` already checks llama-server.exe exists explicitly (line 5297), so the health gate is unchanged. --- install.ps1 | 13 +++---- install.sh | 13 +++---- .../backend/requirements/no-torch-runtime.txt | 18 +++------- studio/install_llama_prebuilt.py | 35 +++++-------------- studio/install_python_stack.py | 21 +++-------- tests/studio/install/test_rocm_support.py | 23 ++++++------ 6 files changed, 38 insertions(+), 85 deletions(-) diff --git a/install.ps1 b/install.ps1 index b26566cc3d..07512de720 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1302,13 +1302,9 @@ shell.Run cmd, 0, False # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.5.6" unsloth-zoo } if ($baseInstallExit -eq 0) { - # Install pydantic WITH deps so pip pins pydantic-core to - # the exact version pydantic's metadata requires. The - # --no-deps install of no-torch-runtime.txt below would - # otherwise pick the latest of each independently and - # trip pydantic's _ensure_pydantic_core_version check. - # pydantic's deps (annotated-types, pydantic-core, - # typing-extensions, typing-inspection) are torch-free. + # Resolve pydantic WITH deps so pip pins pydantic-core + # to the matching version (no-torch-runtime.txt below + # is --no-deps). All transitive deps are torch-free. $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython pydantic } } if ($baseInstallExit -eq 0) { @@ -1358,8 +1354,7 @@ shell.Run cmd, 0, False # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.5.6" unsloth-zoo } if ($baseInstallExit -eq 0) { - # Install pydantic WITH deps so pip pins pydantic-core to - # the matching version (see migrated branch above). + # Same pydantic-with-deps trick as the migrated branch. $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython pydantic } } if ($baseInstallExit -eq 0) { diff --git a/install.sh b/install.sh index 9bdd935171..1e19e951de 100755 --- a/install.sh +++ b/install.sh @@ -1866,13 +1866,9 @@ if [ "$_MIGRATED" = true ]; then run_install_cmd "install unsloth (migrated no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ "unsloth>=2026.5.6" unsloth-zoo - # Install pydantic WITH deps so pip pins pydantic-core to the - # exact version pydantic's own metadata requires. The --no-deps - # install below would otherwise pick the latest of each - # independently and trip pydantic's _ensure_pydantic_core_version - # check on the next import. pydantic's deps (annotated-types, - # pydantic-core, typing-extensions, typing-inspection) are - # torch-free, so this is safe on the no-torch path. + # Resolve pydantic WITH deps so pip pins pydantic-core to the + # matching version (no-torch-runtime.txt below is --no-deps). + # All transitive deps are torch-free. run_install_cmd "install pydantic (with deps for compatible core)" \ uv pip install --python "$_VENV_PY" pydantic _NO_TORCH_RT="$(_find_no_torch_runtime)" @@ -2051,8 +2047,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then run_install_cmd "install unsloth (no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --upgrade-package unsloth --upgrade-package unsloth-zoo \ "unsloth>=2026.5.6" unsloth-zoo - # Install pydantic WITH deps so pip pins pydantic-core to the - # exact version pydantic requires (see migrated branch above). + # Same pydantic-with-deps trick as the migrated branch. run_install_cmd "install pydantic (with deps for compatible core)" \ uv pip install --python "$_VENV_PY" pydantic _NO_TORCH_RT="$(_find_no_torch_runtime)" diff --git a/studio/backend/requirements/no-torch-runtime.txt b/studio/backend/requirements/no-torch-runtime.txt index c33ebf4d94..85294114b1 100644 --- a/studio/backend/requirements/no-torch-runtime.txt +++ b/studio/backend/requirements/no-torch-runtime.txt @@ -22,19 +22,11 @@ rich>=13.0 markdown-it-py>=3.0 mdurl>=0.1 pygments>=2.0 -# pydantic is intentionally NOT installed via this --no-deps file. -# install.sh / install.ps1 / install_python_stack.py run a separate -# `pip install pydantic` (with deps) just before this file is -# applied, so pip resolves `pydantic-core` to the exact version -# pydantic's `_ensure_pydantic_core_version` check expects. Listing -# pydantic + pydantic-core unpinned here and resolving them under -# --no-deps used to pick the latest of each independently and trip -# `SystemError: pydantic-core 2.X.Y is incompatible with the current -# pydantic version` on the first import (Windows fresh-venv repro -# was the canonical case). pydantic's transitive deps -# (annotated-types, pydantic-core, typing-extensions, -# typing-inspection) are torch-free, so installing it WITH deps -# does not pull torch. +# pydantic is intentionally NOT pinned here. install.sh / install.ps1 +# / install_python_stack.py run `pip install pydantic` WITH deps just +# before this --no-deps file is applied, so pip resolves pydantic-core +# to the exact version pydantic's _ensure_pydantic_core_version check +# expects. Pinning both under --no-deps used to drift them apart. pyyaml nest-asyncio diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index 91076a4743..7672af6630 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -3827,37 +3827,18 @@ def paired_runtime_dll_patterns(choice: AssetChoice) -> list[str]: def runtime_patterns_for_choice(choice: AssetChoice) -> list[str]: + # Broad shared-library glob + explicit binary names. Lets upstream + # repackage the SO/DLL set (e.g. ggml-org/llama.cpp#23462 split the + # per-binary entry code into paired ``lib-impl.so`` shared + # libraries between b9279 and b9283) without us re-enumerating + # every new file. Studio only invokes llama-server and llama-quantize; + # other CLIs upstream ships (llama-cli, llama-bench, ...) are skipped. if choice.install_kind in {"linux-cpu", "linux-cuda", "linux-rocm"}: - return [ - "llama-server", - "llama-quantize", - "libllama-common.so*", - "libllama.so*", - # Upstream llama.cpp split the per-binary entry code into - # paired ``libllama--impl.so`` shared libraries - # around release b9261. ``llama-server`` and - # ``llama-quantize`` are NEEDED-linked against - # ``libllama-server-impl.so`` / ``libllama-quantize-impl.so`` - # respectively, with RUNPATH ``$ORIGIN``. Without copying - # the impl ``.so`` files alongside the binaries, ldd - # reports them missing, preflight rejects the install, and - # the installer falls back to a source build on a fresh - # Linux install. Glob the whole family so future bundles - # that split additional binaries (e.g. ``llama-cli``, - # ``llama-bench``) keep working. - "libllama-*-impl.so*", - "libggml.so*", - "libggml-base.so*", - "libmtmd.so*", - "libggml-cpu-*.so*", - "libggml-cuda.so*", - "libggml-hip.so*", - "libggml-rpc.so*", - ] + return ["llama-server", "llama-quantize", "lib*.so*"] if choice.install_kind in {"macos-arm64", "macos-x64"}: return ["llama-server", "llama-quantize", "lib*.dylib"] if choice.install_kind in {"windows-cpu", "windows-cuda", "windows-hip"}: - return ["*.exe", "*.dll"] + return ["llama-server.exe", "llama-quantize.exe", "*.dll"] raise PrebuiltFallback( f"unsupported install kind for runtime overlay: {choice.install_kind}" ) diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index 4dfa20032b..9166d35ce3 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -979,22 +979,11 @@ def install_python_stack() -> int: package_name, "unsloth-zoo", ) - # Pydantic ships its core as a separate compiled wheel - # (pydantic-core), and pydantic's ``_ensure_pydantic_core_version`` - # checks the installed core matches the exact version pinned in - # its own metadata. With ``--no-deps`` plus an unpinned - # ``pydantic`` / ``pydantic-core`` pair in no-torch-runtime.txt, - # pip resolved each to the newest available version and the two - # drifted (pydantic 2.13.4 pins pydantic-core==2.46.4 today, but - # pydantic-core 2.47.0 was the latest). On a fresh Windows venv - # the next ``import pydantic`` raised ``SystemError: ... - # incompatible with the current pydantic version``. - # - # Resolve them WITH deps in a focused pip call so pip picks a - # compatible pair. pydantic's own deps are - # ``annotated-types``, ``pydantic-core``, ``typing-extensions``, - # ``typing-inspection`` -- none of which transitively pull - # torch, so this is safe for the no-torch path. + # Resolve pydantic WITH deps so pip pins pydantic-core to the + # exact version pydantic's metadata declares. Under --no-deps + # alone pip picks the latest of each and trips pydantic's + # _ensure_pydantic_core_version check. Transitive deps are + # torch-free. pip_install( "Installing pydantic (with deps for compatible core)", "--no-cache-dir", diff --git a/tests/studio/install/test_rocm_support.py b/tests/studio/install/test_rocm_support.py index 698625e59b..e6f1ae1c65 100644 --- a/tests/studio/install/test_rocm_support.py +++ b/tests/studio/install/test_rocm_support.py @@ -346,28 +346,26 @@ class TestRuntimePatterns: patterns = runtime_patterns_for_choice(choice) assert "llama-server" in patterns assert "llama-quantize" in patterns - # Upstream split entry code into ``libllama--impl.so`` - # shared libraries (b9261+). llama-server and llama-quantize - # are NEEDED-linked against ``libllama-server-impl.so`` and - # ``libllama-quantize-impl.so`` respectively with RUNPATH - # ``$ORIGIN``, so the prebuilt overlay MUST copy them - # alongside the binaries or ldd reports them missing and - # preflight forces a source-build fallback. - assert "libllama-*-impl.so*" in patterns + # Broad lib*.so* covers libllama, libggml, libmtmd, libggml-cpu-*, + # plus the libllama--impl.so split that ggml-org/llama.cpp + # #23462 introduced between b9279 and b9283. + assert "lib*.so*" in patterns def test_linux_cuda_patterns(self): choice = AssetChoice( repo = "", tag = "", name = "", url = "", source_label = "", install_kind = "linux-cuda" ) patterns = runtime_patterns_for_choice(choice) - assert "libggml-cuda.so*" in patterns + # libggml-cuda.so is matched by lib*.so* now. + assert "lib*.so*" in patterns def test_linux_rocm_patterns(self): choice = AssetChoice( repo = "", tag = "", name = "", url = "", source_label = "", install_kind = "linux-rocm" ) patterns = runtime_patterns_for_choice(choice) - assert "libggml-hip.so*" in patterns + # libggml-hip.so is matched by lib*.so* now. + assert "lib*.so*" in patterns assert "llama-server" in patterns def test_windows_hip_patterns(self): @@ -380,7 +378,10 @@ class TestRuntimePatterns: install_kind = "windows-hip", ) patterns = runtime_patterns_for_choice(choice) - assert "*.exe" in patterns + # Narrowed from "*.exe" to the two binaries Studio actually + # invokes, mirroring the Linux/macOS pattern style. + assert "llama-server.exe" in patterns + assert "llama-quantize.exe" in patterns assert "*.dll" in patterns def test_macos_patterns(self): From 56e9046b2ffd9d6e54d9cd25d4f37d7f24f7ed4e Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 23 May 2026 22:28:59 -0700 Subject: [PATCH 4/6] Lower default weight_decay in RL config from 0.01 to 0.001 (#5747) In full FT, AdamW weight decay shrinks the parameter directly so the implicit prior is W -> 0. In LoRA the trained parameters are A and B while the effective weight is W = W_init + (alpha/r) * B @ A; decaying A and B separately drives BA -> 0, hence W -> W_init rather than 0. The previous default of 0.01 inherited from full-FT recipes adds a measurable pull on the merged adapter back toward the base model over a few thousand steps. 0.001 keeps a small Frobenius-norm prior on ||A||^2 + ||B||^2 for numerical stability without meaningfully biasing the merged weight toward init, and aligns with the value used across the unsloth notebook templates. --- unsloth/models/rl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 7b7c3ac1a4..c82c8364b3 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -1312,7 +1312,9 @@ def _patch_trl_rl_trainers_impl(trainer_file = "grpo_trainer"): "logging_nan_inf_filter": False, "per_device_train_batch_size": 4, "gradient_accumulation_steps": 2, - "weight_decay": 0.01, + # LoRA decays A and B toward 0 so effective W = W_init + (alpha/r) * B @ A is pulled toward W_init, not 0 as in full FT. + # 0.001 keeps a small Frobenius prior |A|_F^2 + |B|_F^2 without measurably dragging the merged adapter back to base. + "weight_decay": 0.001, "seed": 3407, "optim": "adamw_8bit", "learning_rate": 5e-05, From f7f540a58b853d262ec7ba90c9c0af5e742cc696 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 24 May 2026 05:00:08 -0700 Subject: [PATCH 5/6] Studio: strip orphan tool_call XML leaking into visible content (#5735) * Studio: strip orphan tool_call XML from streamed visible content The speculative-buffer state machine in `studio/backend/core/inference/llama_cpp.py` can slice a tool_call XML block between the silent DRAINING path and the user-visible content_accum, depending on when in the model's emission the BUFFERING -> STREAMING -> DRAINING transitions fire. Three leak shapes were observed in a 2026-05-22 sweep of 900 Qwen3.5 / Qwen3.6 GGUF runs: Pre-fix XML leak rate: 20/900 (2.22%), concentrated 6.7% on the larger Q8 / MTP configs: Qwen3.6-35B-A3B Q8_0 4/60 (6.7%) Qwen3.6-35B-A3B-MTP Q4 4/60 (6.7%) Qwen3.5-35B-A3B Q8_0 3/60 (5.0%) Qwen3.6-27B Q8_0 3/60 (5.0%) The existing `_TOOL_XML_RE` only matched well-formed `...` and `` pairs, so unterminated openings (close was DRAINED) and orphan closes (opening was DRAINED) survived the strip and reached the user. Fix relaxes the regex to also strip: 1. Orphan opening up to end-of-string: `(?:|\Z)` 2. Orphan closing tag: bare `` / `` Verified on the full sweep: 20/900 -> 0/900 (100% of detected leaks eliminated). 16 unit tests in `test_tool_xml_strip.py` pin all three leak shapes plus the well-formed cases, plus parametrised checks on the 5 actual real-world leak samples from the sweep data. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: strip tail-only orphan + tighten regex The 2026-05-22 gdpval sweep surfaced a 4th XML-leak shape not caught by the earlier regex: a bare `\n\n` at end-of-buffer (7 of 192 trials, all Qwen3.5-27B + a few Qwen3.6-27B). The model emits the full `...content... ` envelope, the speculative buffer DRAINS the opening tags as intended, but EOS (max_tokens cutoff) truncates the outer `` close, leaving just `` as the visible tail. We strip this ONLY when end-anchored (`\s*\Z`) so legitimate mid-text uses (user code samples, documentation discussing the Qwen tool-call XML shape) survive. Verified on the 192-trial gdpval corpus: before=7, after=0. While at it, fold the five top-level alternations into three by sharing tag-name and prefix subgroups: ... + ... + --> <(?:tool_call|function=\w+)>... | --> Semantically identical (verified by replay over the 192-trial corpus + adversarial inputs, 0 diffs) and 1.34x faster on real workloads. Backtracking-safety pinned by two new perf guards (256KB '<' spam, 1000x orphan opens). Tests: 16 -> 28 (6 new functional + 4 well-formed-vs-orphan + 2 perf guards). * Tighten comments in XML-strip regex and tests Code says what it does; comments were repeating it. Strip the verbose explanations down to the WHY-only bits (engine quirk, tail-anchor rationale, real-world source of each test sample). No code changes. inference.py: 21 -> 12 lines around _TOOL_XML_RE test_tool_xml_strip.py: 343 -> 259 lines (-84) Tests: 28/28 still pass. * [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> --- studio/backend/routes/inference.py | 12 +- studio/backend/tests/test_tool_xml_strip.py | 263 ++++++++++++++++++++ 2 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 studio/backend/tests/test_tool_xml_strip.py diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 02270ab405..bf92055929 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -427,9 +427,17 @@ _TOOL_ACTION_NUDGE = ( " Do NOT output code blocks -- use the python tool instead." ) -# Regex for stripping leaked tool-call XML from assistant messages/stream +# Strip tool-call XML the speculative buffer in core/inference/llama_cpp.py +# split across the visible/DRAIN boundary. Four leak shapes: +# 1. well-formed `...` / `...` +# 2. orphan opening to EOF (close was DRAINED) +# 3. bare orphan close (open was DRAINED) +# 4. tail-only `` (outer close truncated by EOS); anchored to +# `\Z` so mid-text `` in user code samples survives. _TOOL_XML_RE = _re.compile( - r".*?|.*?", + r"<(?:tool_call|function=\w+)>.*?(?:|\Z)" + r"|" + r"|\s*\Z", _re.DOTALL, ) logger = get_logger(__name__) diff --git a/studio/backend/tests/test_tool_xml_strip.py b/studio/backend/tests/test_tool_xml_strip.py new file mode 100644 index 0000000000..8b90a46d5a --- /dev/null +++ b/studio/backend/tests/test_tool_xml_strip.py @@ -0,0 +1,263 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""Tests for `_TOOL_XML_RE` (routes/inference.py) -- strips tool-call +XML that leaks past the speculative buffer in core/inference/llama_cpp.py +when the open/close pair is split across the visible/DRAIN boundary. +""" + +from __future__ import annotations + +import sys +import types as _types +from pathlib import Path + +import pytest + +_BACKEND_DIR = str(Path(__file__).resolve().parent.parent) +if _BACKEND_DIR not in sys.path: + sys.path.insert(0, _BACKEND_DIR) + +# Extract the regex from source (routes module needs heavy stubbing to import). +import re as _re + +_src = (Path(_BACKEND_DIR) / "routes" / "inference.py").read_text() +_m = _re.search(r"_TOOL_XML_RE = _re\.compile\((.*?)\n\)", _src, _re.DOTALL) +assert _m, "could not extract _TOOL_XML_RE source" +_ns = {"_re": _re} +exec(f"_TOOL_XML_RE = _re.compile({_m.group(1)})", _ns) +_TOOL_XML_RE = _ns["_TOOL_XML_RE"] + + +# ── Well-formed pairs ───────────────────────────────────────────── + + +def test_strips_well_formed_tool_call(): + text = ( + "Let me search.\n" + "\n" + "\n" + "\nBillboard 2015\n\n" + "\n" + "\n" + "Here are the songs:" + ) + cleaned = _TOOL_XML_RE.sub("", text) + assert "" not in cleaned + assert "" not in cleaned + assert "" not in cleaned + assert "Here are the songs:" in cleaned, "non-XML content must survive" + assert "Let me search." in cleaned + + +def test_strips_function_only_well_formed(): + text = "Setup.\n\n\nprint(1)\n\n\nDone." + cleaned = _TOOL_XML_RE.sub("", text) + assert "" + "\n" + "\n" + "\nBillboard 2015\n\n" + "" not in cleaned + assert "\n\nprint(1)\n" + ) + cleaned = _TOOL_XML_RE.sub("", text) + assert "") + assert "" not in cleaned + assert "Search starting." in cleaned + + +def test_strips_multiple_orphans(): + text = ( + "First call:\n\n\n\nx=1\n" + "Second call:\n\n\nhi\n" + ) + cleaned = _TOOL_XML_RE.sub("", text) + assert "" not in cleaned + assert "" not in cleaned + assert "" not in cleaned + # Mid-string intentionally preserved (see preserve test). + + +# ── Tail-only (PR #5735 follow-up) ─────────────────── + + +def test_strips_tail_only_parameter_orphan(): + # Outer truncated by EOS, inner DRAINED. + cleaned = _TOOL_XML_RE.sub("", "and the text is not readable.\n\n\n") + assert "" not in cleaned + assert "and the text is not readable." in cleaned + + +def test_strips_tail_only_parameter_orphan_single_newline(): + cleaned = _TOOL_XML_RE.sub("", "Global Economic Prospects\n\n") + assert "" not in cleaned + assert "Global Economic Prospects" in cleaned + + +def test_strips_tail_only_parameter_orphan_no_trailing_ws(): + cleaned = _TOOL_XML_RE.sub("", "Final answer.") + assert "" not in cleaned + assert "Final answer." in cleaned + + +def test_preserves_mid_string_parameter_in_code_sample(): + # Tail-anchor on `` is required so doc/example prose survives. + text = ( + "Here is the Qwen tool-call format:\n" + "```xml\n" + "value\n" + "```\n" + "Note the closing sits inside ." + ) + cleaned = _TOOL_XML_RE.sub("", text) + assert "Note the closing sits inside" in cleaned + + +def test_strips_well_formed_then_orphan(): + text = ( + "Round one:\n\n\n\n1\n" + "\n\n\n" + "Now round two:\n\n\n\n" + "what is X\n\n" not in cleaned + assert "\n\n\n"Billboard Hot 100" "2015" "weekly" "chart" "position" "3"\n\n\n\n\n"peaked at number 3" Billboard Hot 100 2015 list\n\n\n\n\n"List of Billboard Hot 100 top-ten singles in 2015" wikipedia\n\n\n\nThe user wants me to list and categorize all songs that charted #3 on the Billboard Hot 100 in 2015. I have been trying to get this data", + # Qwen3.6-35B-A3B Q8_0 billboard s21 -- orphan close + "parse it more carefully.\n\n\nThe user wants a list of songs that charted #3 on the Billboard Hot 100 in 2015, categorized.", +] + + +@pytest.mark.parametrize( + "leak", REAL_LEAKS, ids = [f"sweep_sample_{i}" for i in range(len(REAL_LEAKS))] +) +def test_real_world_sweep_leaks_get_stripped(leak): + cleaned = _TOOL_XML_RE.sub("", leak) + assert "" not in cleaned, f"leak survived: {cleaned!r}" + assert " from gdpval sweep ────────── + + +# All end-anchored: outer truncated by EOS, +# inner open DRAINED, leaving bare tail. +GDPVAL_PARAMETER_LEAKS = [ + # Qwen3.5-27B Q8_0 / worldbank s00 + "the page contains image data and the text is not readable.\n\n\n", + # Qwen3.5-27B Q8_0 / worldbank s42 (preceded by mojibake) + "...some mojibake content here...\n\n\n", + # Qwen3.5-27B UD-Q4_K_XL / coppa s07 + "blocked, while others may still be in effect. The law is currently under further review by the Ninth Circuit.\n\n\n", + # Qwen3.5-27B UD-Q4_K_XL / police_training s00 + "comprehensive training report\n\n\n", + # Qwen3.5-27B UD-Q4_K_XL / worldbank s00 + "Global Economic Prospects\nJune 2025\nGlobal Economic Prospects\n\n", + # Qwen3.6-27B Q8_0 / overpass s07 + "Let me create a comprehensive query and instructions document.\n\n\n", +] + + +@pytest.mark.parametrize( + "leak", + GDPVAL_PARAMETER_LEAKS, + ids = [f"gdpval_param_orphan_{i}" for i in range(len(GDPVAL_PARAMETER_LEAKS))], +) +def test_gdpval_parameter_orphans_get_stripped(leak): + cleaned = _TOOL_XML_RE.sub("", leak) + assert "" not in cleaned, f"leak survived: {cleaned!r}" + + +# ── Backtracking guards ────────────────────────────────────────── + + +def test_no_catastrophic_backtracking_on_open_bracket_spam(): + # 256KB of '<' must fail fast (literal mismatch char 2), not backtrack. + import time + + adv = "<" * (1024 * 256) + "X" + t0 = time.perf_counter() + _TOOL_XML_RE.sub("", adv) + elapsed = time.perf_counter() - t0 + assert elapsed < 0.5, f"regex took {elapsed*1000:.0f}ms on 256KB '<' spam" + + +def test_no_catastrophic_backtracking_on_orphan_opening_spam(): + # 1000 unclosed openings: first alt must consume them all greedily. + import time + + adv = "X" * 1000 + t0 = time.perf_counter() + cleaned = _TOOL_XML_RE.sub("", adv) + elapsed = time.perf_counter() - t0 + assert elapsed < 0.1, f"regex took {elapsed*1000:.0f}ms on 1000x orphan opens" + assert "" not in cleaned From 9c5d751c667c874b519f5dc031320b01b709bf4b Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 24 May 2026 07:11:21 -0700 Subject: [PATCH 6/6] Fixes --- unsloth/models/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 3b67c0f487..b940fdf35a 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2026.5.6" +__version__ = "2026.5.7" __all__ = [ "SUPPORTS_BFLOAT16",