mirror of
https://github.com/unslothai/unsloth.git
synced 2026-10-20 11:27:45 +00:00
* Repair a quarantined llama.cpp runtime at launch instead of failing at model load Smart App Control and antivirus quarantine individual files out of the managed llama.cpp tree while leaving the marker and the rest of the tree in place. The desktop's preflight decided staleness on the managed Python alone, so it reported Ready, launched, and the missing DLL only surfaced later at model load as an error that looked like a bad GGUF or an out of memory. Repairing it meant running `unsloth studio update` by hand, which nothing told the user to do. install_llama_prebuilt gains installed_runtime_health(), which reuses the same payload tables the keep-install path already uses rather than duplicating the required file list. The CLI reports it in the desktop capability payload and the Tauri preflight treats an explicit false as stale, so the existing repair path picks it up. Only an explicit false is stale. None covers both "no runtime installed yet" and "this CLI predates the field", and neither is a broken runtime: treating them as stale would send every existing install through repair on its first launch after an upgrade. For the same reason desktop_manageability_version is not bumped. The capability cache had to move too. Its fingerprint covered the managed venv only, so a file quarantined out of the runtime left it identical and preflight answered Ready from cache without ever asking the CLI. The runtime's file count and total size now join the fingerprint, and the cache schema goes to 4. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Close the gaps the simulations found, and prove the repair cannot loop The launch probe has to be no stricter than the setup scripts' own keep-or-reinstall decision. A tree the probe rejects but _existing_install_runs keeps would be repaired, kept unchanged by the repair and rejected again on the next launch, which is a loop with no way out. That property is now stated where the code is and tested as a property over the whole matrix. Three real gaps closed: The payload groups are libraries, and on Linux and macOS they name no executable at all, so a quarantined llama-server passed as a complete install. Both entrypoints are now looked for, which is what _existing_install_runs already requires, so the check stays on the safe side of the rule above. platform_only_host() left macos_version unset while detect_host() derives it from platform.mac_ver(), which reads no hardware. The parity test would have failed on a macOS runner. A relative UNSLOTH_LLAMA_CPP_PATH resolved against the desktop's working directory, which is not the one the CLI child runs under. Fingerprinting whatever sits beside the desktop is worse than not fingerprinting, so it now degrades to no coverage. A leading ~ is expanded, since the Python reader calls expanduser. Deliberately not changed, and pinned by a test so it is not "fixed" later: a marker that exists but does not parse still reads as nothing installed. Reporting it broken looks right, but confirm_install_tree only checks that the marker file exists, so the offline keep path keeps such a tree, and broken plus kept is exactly the loop above. Tests. 1361 cases over [Windows, Linux, macOS] x [cpu, cuda, rocm, vulkan, metal] x thirteen shipped marker shapes x damage modes, asserting both the verdict and the invariant, with the invariant's sensitivity checked by mutation rather than assumed. Healthy installs of every shipped shape stay healthy on every platform, which is the backwards-compatibility half. The verdict is unchanged across NVIDIA, AMD, Intel and no GPU at all. Rust gains JSON round trips proving a pre-PR payload still parses and reads Ready, that an old cache file is ignored rather than fatal, and that a newer CLI's unknown keys are inert. The frontend no longer tells a user with a quarantined runtime that their install is too old. * Format the new install-health files with the pinned ruff scripts/run_ruff_format.py runs whatever ruff is on the path; the hook pins 0.6.9, and these four files were written under a newer one. The two disagree about assert-message wrapping and about when a def signature goes one parameter per line, which is what pre-commit.ci reported as "files were modified by this hook". * Fix the review round, and two defects the simulations found under it Seven review items were genuine and are fixed here. Two of them contradicted calls I had made earlier and the review was right both times. A marker that exists but does not parse is now graded rather than reported as nothing installed. I had argued the opposite, on the grounds that the offline keep path keeps such a tree and broken plus kept is a loop. The narrower split is the correct one: an absent marker file is a runtime nobody installed, a present one that will not parse is a real tree, and when it has lost a library as well, answering None left preflight Ready with no repair offered. The keep path already reads such a marker as an unknown backend and grades the same payload, so nothing here asks for more than it does. A relative UNSLOTH_LLAMA_CPP_PATH was falling through to the legacy tree, which is a regression I introduced in the commit that was meant to prevent exactly that. Dropping it to None is not the fix either: None matches None, so the cache keeps hitting while the real runtime rots. It is anchored to this process's working directory now, which is where relative_override_pins joins the child's copy from. On Windows the tilde resolves through USERPROFILE rather than dirs::home_dir(), because that is what the child uses and a portable profile moves the second one; process.rs carries the same note at its own read. A quarantined llama-server was already covered. Managed runtime health is now reported only when the managed runtime is the one that would actually load, since _find_llama_server_binary prefers LLAMA_SERVER_PATH and the folder chosen in settings ahead of it, and grading the managed tree regardless sent a user running their own build into a repair that offline cannot even succeed. Two further defects came out of the simulations rather than the review. A dead Linux runtime was reported healthy. A release ships libllama.so.0 beside libllama.so.0.0.10360 and the group pattern matches both, so quarantining the SONAME, which is the name the loader asks for, left the group satisfied by the twin. Measured against a real install: llama-server exits 127 with a loader error while the probe said healthy. No fixture could show it, because a fixture writes one file per library and a release writes two. Dangling symlinks in a versioned chain were passing for the same reason and are covered by the same check. The repair could not terminate. setup.sh skipped the source rebuild whenever the two entrypoints were executable, which is true of every tree that lost only a library, so offline the repair returned the tree byte for byte identical and reported success while the next launch rejected it again. It asks reusable_existing_install first now. That branch only runs once the prebuilt path has failed, where keeping a tree that cannot load is never right, and a tree with no marker is a real source build and keeps the old test. Tests: 5965 in tests/studio/install, 561 in the Tauri crate. The convergence census that pinned the loop as a fact now pins its absence, and the strict xfail holding the SONAME defect is an ordinary assertion. * Give a runtime tree with no build/bin its own fingerprint read_dir(build/bin).ok()? answered None for two different states: no llama.cpp runtime at all, and a runtime tree whose binary directory is gone. The capability verdict is not the same in both. With no marker, installed_runtime_health answers None, llama_runtime_ok stays null, and desktop_capability_stale_reason deliberately does not call that stale, so a Ready gets cached. Once a marker sits over a missing build/bin the CLI would answer llama_runtime_dir_missing, but the fingerprint had not moved, the cache still matched, and preflight served the old Ready without ever asking. The root's own entries now stand in when build/bin cannot be read, so the marker arriving moves the key. None still means the tree is not there, which is the NotInstalled case. * Catch up the fingerprint tests with the bin and nobin prefixes The previous commit gave a runtime tree with no build/bin its own fingerprint and prefixed the walk, which left five assertions on the old shape and the crate red at 557 of 562. Three are the format: bin: now leads the count. Two are the semantics the change introduced, and they are worth stating rather than patching. "No runtime at all" is the absence of the tree, so its test points at a path that does not exist instead of an empty directory, which is now the broken-install state. A root that is present but whose binary directory cannot be read is broken too, not absent: sharing None with a machine that never installed a runtime is exactly what let a cache written before an install keep matching after it broke. * Tighten the comments on the runtime health preflight Shorten every comment and docstring this branch added, keeping the reasoning that is load bearing and dropping the restatements of the code beside them. No code, only comments and whitespace. * Address review: per-library macOS payload groups, ~user paths, blank runtime override - installed_runtime_health now checks each macOS library on its own instead of three broad globs, so a bundle missing libllama-common, libggml-base or libggml-cpu is graded incomplete rather than healthy. Verified against the real macos-arm64 bundle: complete is healthy, removing any one library is not, and dropping the metal, blas and rpc backends stays healthy. - llama_runtime_override_from resolves ~user/... through getpwnam on unix rather than treating it as a relative path. - A blank UNSLOTH_LLAMA_CPP_PATH no longer counts as an override. - The CLI capability payload only reports the managed runtime when the managed runtime is the one that would actually run. Fixtures that built macOS trees from the old broad names now write the names a real bundle ships. * Address review: macOS SONAME links, the execute bit, and which runtime is graded - A dylib chain now follows the same rule as an .so: losing the middle libggml.0.dylib left the group satisfied by the terminal libggml.0.23.0.dylib while dyld could not launch, since the LC_LOAD_DYLIB entry names the link. - llama-server and llama-quantize must still carry the execute bit on POSIX. _existing_install_runs already asks that way, so an install with the bit cleared was rejected there and reported healthy here. - The CLI capability helper follows the finder's own file status rather than the directory entry, so a pinned symlink whose target was quarantined falls through to the managed tree the backend really loads instead of suppressing its verdict. - UNSLOTH_LLAMA_CPP_PATH outranks the stored custom folder in the finder and default_managed_llama_dir points at exactly that tree, so it is graded even when an older selection is still in the settings database. - ~name on Windows resolves to the sibling profile the same way process.rs and ntpath.expanduser resolve it, instead of being fingerprinted under the cwd. - A runtime verdict the CLI skipped is no longer cached, since nothing in the fingerprint watches which runtime is selected and clearing a custom selection would otherwise keep serving a Ready nobody had graded. * Ask for the execute bit by the tree's platform, not the interpreter's A Windows bundle unpacked on a POSIX filesystem carries no execute bit, and the host is a parameter here, so os.access X_OK graded every real windows-x64 bundle in the history suite as missing its binaries. Windows has no execute bit for os.access to read anyway, so presence is the whole question there. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address review: the fingerprint's blind spots, and which tree it watches - The runtime fingerprint counted regular files only, so deleting an install-name link (libggml.0.dylib, or the SONAME link on Linux) or taking the execute bit off llama-server left it identical. Both are states installed_runtime_health rejects, so a cached Ready went on matching a tree that could no longer launch. Links and executable files are counted now. - An installer-managed venv under a custom root names its own runtime. The desktop strips UNSLOTH_STUDIO_HOME before spawning the CLI, but the CLI re-infers the root from sys.prefix and points UNSLOTH_LLAMA_CPP_PATH at its llama.cpp, so reading only the ambient environment fingerprinted the legacy tree and quarantine in the real one never invalidated anything. - An override holding no server does not end the search, so it is no longer treated as final on either side. The CLI names the tree it grades rather than answering yes or no, so the two cannot pick different trees. - ~name resolves through getpwnam_r. getpwnam hands back process-global storage and this runs on a Tauri worker thread, where a concurrent lookup could overwrite pw_dir mid-copy. * Address review: entrypoints that are files, the mode bits, and one override spelling installed_runtime_health asked os.access(X_OK), which a directory named llama-server answers true for because directories are searchable, while the finder asks is_file() and rejects the tree. Both now go through one helper that _existing_install_runs shares, so the keep-or-reinstall decision and the launch verdict cannot disagree. The runtime fingerprint counted files with any execute bit, but os.access(X_OK) asks whether this user may run one: 0755 to 0655 leaves group and other executable and the owner unable to run it, so the count never moved for a state health rejects. The permission bits themselves are summed instead. _find_llama_server_binary read UNSLOTH_LLAMA_CPP_PATH literally while default_managed_llama_dir, get_stored_custom_llama_cpp_path and the desktop's pinning all expand it, so a ~/llama.cpp written into a service unit or a .env sent discovery to a folder named ~ beside the working directory and every other component reported on a different tree. It expands now, and so does the orphan reaper's allowlist, which has to name the tree discovery spawns from. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address review: the split entrypoint libraries, and reuse asking what the probe asks Two of the three items from the last round, both measured against a b10360 managed install rather than a fixture. 1. The Linux impl split had no payload group. ggml-org/llama.cpp#23462 moved each binary's entry code into a lib<binary>-impl library. Windows already required llama-server-impl.dll, gated on the build the split landed in. Linux got the same treatment upstream and none of it here: the groups name the shared libraries only, so libllama-server-impl.so and libllama-quantize-impl.so were required by nothing. Measured on a copy of the managed install, removing either one: installed_runtime_health -> (True, "") _existing_install_runs -> False which is the disagreement the whole probe exists to remove, in the direction that hurts: preflight says Ready and llama-server dies in the loader. _linux_split_entrypoint_groups adds both, gated the same way the Windows side is and on the same build number, since it is one upstream commit rather than one platform's packaging. A source build ships neither whatever the tag says, and a pre-split archive is healthy without them, so requiring one of those would reinstall on every check forever. 2. The exact-release reuse decision asked exists() where the probe asks for the execute bit. installed_runtime_health asks _entrypoint_is_runnable, because that is what _find_llama_server_binary asks. existing_install_matches_choice asked exists(), and its Linux ldd gate reads a non-executable ELF quite happily. Clearing the execute bit on llama-server, again on a real copy: installed_runtime_health -> (False, "llama_runtime_binaries_missing") _existing_install_runs -> False exists() -> True preflight_linux_installed_binaries -> passes so preflight says broken, repair says the exact release is already installed, nothing is downloaded, and the next launch says broken again. Both attempts land in the same place. It now calls _entrypoint_is_runnable, which is the same test both other answers use. Tests: - test_quarantining_a_split_entrypoint_library_is_reported_broken, per library, against a managed copy, asserting the two answers agree afterwards. - test_an_older_monolithic_linux_release_is_not_asked_for_the_impl_libraries, which pins the gate rather than the requirement: b9279 owes neither, b9283 owes both, a source build owes neither. - test_a_stripped_execute_bit_is_not_reused_as_an_exact_release_match, checking the two gates directly. - The fixture tables gain both libraries, and the shape writers now chmod the entrypoints, which a real extraction does and the reuse decision now reads. tests/studio/install: 5991 passed, 6 skipped. cargo test --bin unsloth-studio: 571 passed. The third item, a versionless libllama.so surviving its SONAME's removal, does not apply: the release ships exactly two names per library, libllama.so.0 beside libllama.so.0.0.10360, with no versionless file and no symlinks. Replied on the thread. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address review: resolve a named-user override the same way on both sides of a spawn preflight::managed resolves UNSLOTH_LLAMA_CPP_PATH=~alice/llama.cpp through getpwnam_r, the way posixpath.expanduser does. process.rs, pinning the same variable for the child it is about to move, read it with the Windows rule, which leaves ~alice alone off Windows, so it fell through to being anchored under the directory being left. Measured, with the two halves side by side on this host: preflight::managed ~root/llama.cpp -> /root/llama.cpp process.rs pin ~root/llama.cpp -> /mnt/work/session/~root/llama.cpp The child is handed the pinned value, so it grades and runs one tree while the cache fingerprints another. Quarantining a file from the tree actually in use then never invalidates a Ready verdict, which is the whole failure this fingerprint exists to catch. expand_user now makes the split every other reader in that function already makes, Windows rules on Windows and the native ones off it, and expand_posix_user resolves the named form through the same lookup preflight::managed uses, which is now pub(crate) rather than duplicated. An unknown name is left exactly as it arrived, which is what expanduser does, so both halves fall through to anchoring it and still agree. Bare ~ and ~/rest are unchanged: they went through expand_windows_user's empty-name arm, which answers home on any platform, and they go through the posix one now. Windows was already aligned. named_user_home's Windows arm delegates to named_windows_user_home, which is the same rule expand_windows_user applies. Tests, all three unix-only since they turn on the password database: - a_named_user_override_is_pinned_where_the_fingerprint_watches, asserting the pin is not under the directory being left and equals what the fingerprint side reaches through the same lookup. - an_unknown_named_user_is_left_alone_and_anchored_by_both_halves. - a_bare_tilde_override_still_reaches_home_off_windows, for the form that already worked. cargo test --bin unsloth-studio: 574 passed. tests/studio/install: 5991 passed, 6 skipped. * Address review: the SONAME beside a flattened versionless copy, and an override that cannot expand Two items, and the first one corrects a rebuttal of mine that was wrong. 1. A versionless copy kept the group satisfied after the SONAME was quarantined. I argued against this last round on the grounds that the bundle ships exactly two names per library. That was true of the tree I measured, the b10360 install on this machine, and it is not true of what the installer produces today. Two things moved underneath it: - b10840 ships the trio. libllama.so -> libllama.so.0 -> libllama.so.0.4.0, as symlinks in the tarball. - copy_globs flattens them. It selects with is_file() and copies with shutil.copy2, which follows symlinks, so all three arrive in build/bin as independent regular files. My reproduction moved the extracted tree instead, which kept the links, and a dangling link is dropped by is_file() on its own. That made the argument look decisive about a shape no install has. Extracting b10840 with prebuilt_core.extract_archive and installing it through copy_globs, which is the path install_prebuilt takes: baseline health=(True, "") quarantine libllama.so.0 health=(True, "") runs=False quarantine libggml.so.0 health=(True, "") runs=False quarantine libllama-common.so.0 health=(True, "") runs=False quarantine libggml-base.so.0 health=(True, "") runs=False quarantine libmtmd.so.0 health=(True, "") runs=False _payload_match_is_loadable can no longer answer from the name alone, because libllama.so means one thing in a family that has a SONAME and another in a family that never had one. It now reads the family: a versionless name counts only when nothing versioned sits beside it. Any versioned sibling disqualifies it, not only a SONAME-shaped one, since asking for a SONAME specifically would read the family as versionless again exactly when the SONAME is the file that went missing. Same rule on macOS, where dyld asks for the install name in LC_ID_DYLIB: libggml.dylib does not stand in for libggml.0.dylib. 2. An unresolvable named user made runtime discovery throw. Path.expanduser() raises RuntimeError when it cannot determine the home, which includes every unknown named user, unlike os.path.expanduser, which hands the value back: Path("~no-such-account-anywhere/llama.cpp").expanduser() RuntimeError: Could not determine home directory. Every reader of UNSLOTH_LLAMA_CPP_PATH used the raising form, so a pin left in a service unit or a .env after an account rename took discovery down with it rather than being treated as unusable, and several callers have no exception boundary. Four sites, not the one reported: the search, the process allowlist that has to name the same tree, the settings reader and the status payload the UI renders. expanded_user_path is the non-raising expansion, shared by all four so they cannot diverge, and it leaves an unresolvable name exactly as it arrived, which is what the desktop's pinning does with one too. Tests: - test_the_flattened_trio_installs_healthy and test_quarantining_a_soname_beside_a_versionless_copy_is_reported_broken, per library, built from the b10840 asset through copy_globs, asserting the three names really are flattened regular files first, so the test cannot pass for the dangling-link reason instead. Skipped where the asset is absent, as the rest of that file is. - test_a_family_that_only_ever_ships_one_name_is_still_loadable and the macOS counterpart, both artifact-free, for the half a blunter rule would break: libggml-cpu-x64.so has no versioned copy anywhere and is the name the loader asks for. - The expanduser guard, including the status payload and discovery reaching its own answer rather than raising. - test_a_dangling_library_symlink_does_not_count_as_present now builds all three rungs, which is what its own docstring already described; it had been building the outer two, and the SONAME rung is what the new rule needs to see. tests/studio/install: 5999 passed, 6 skipped. studio/backend/tests -k "llama_cpp or path_settings": 1928 passed, 4 skipped. cargo test --bin unsloth-studio: 574 passed. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address review: the HIP module by name, both impl halves, and a marker that will not parse Six items, each measured against a real bundle built through copy_globs rather than reasoned about. ggml-hip.dll by name, not "*hip*.dll". app-b10798-mix-659e406-windows-x64-rocm-gfx1150 ships amdhip64_7.dll, hipblas.dll and libhipblaslt.dll beside it, and all four match that pattern, so quarantining the one module ggml actually loads left the group satisfied by three libraries that cannot stand in for it. Measured: installed_runtime_health answered (True, "") with the backend gone. llama-quantize-impl.dll under the same split-build gate as the server's. llama-quantize.exe links against its own impl library exactly as llama-server.exe does, and every post-b9283 bundle ships both, so requiring one let a quarantined quantize implementation read as healthy while quantization could not start. A marker that exists but will not parse names no source, and the source-gated groups were dropped with it: on Windows llama.dll stood in for the whole payload, so an interrupted marker plus a quarantined ggml-base.dll answered healthy. Assuming "published" instead would be worse, because a statically linked source build ships none of those names and would fail health on a tree the setup scripts keep, repairing on every launch with nothing to change. _tree_looks_prebuilt reads the source off the tree from a name only a release ships, and stays lenient when quarantine has taken the evidence too. The runtime fingerprint now includes entry names. Its four counters are all aggregates, and a rename in place moves none of them, so security software that renames ggml-base.dll beside itself left the cached Ready matching a tree installed_runtime_health rejects, and preflight answered Ready without running the capability probe. FNV-1a rather than DefaultHasher, so a toolchain bump does not invalidate every cached verdict; summed rather than folded, because read_dir order is unspecified and the counters beside it are order-independent for that reason. The CLI classifies UNSLOTH_LLAMA_CPP_PATH instead of trusting the marker. studio/backend/main.py calls mark_managed_llama_cpp_path before discovery, which marks any override equal to the managed tree however it got there, and the finder then skips it; _ensure_studio_env_exported writes exactly that value under a custom STUDIO_HOME and sets no marker. So preflight graded the managed tree as a user pin while the backend walked past it to the stored selection, blocking launch over a tree nothing would open. Compared against the studio home's own path with the override out of the way, never default_managed_llama_dir(), which reads the override first and would call every user pin managed. expanded_user_path at the last Path.expanduser left in the doctor, a gap in the earlier pass: a "~name" naming no account raises RuntimeError out of a command whose whole job is to answer. Also: the stored-selection stub in test_desktop_capability_llama_runtime was missing expanded_user_path, so the helper's import raised and every test in that file was silently grading the except fallback rather than the code it names. tests/studio: 10171 passed, 198 skipped. cargo test --bin unsloth-studio: 575 passed. The four new payload tests and the CLI one all fail on the previous head. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Gate the PowerShell reuse shortcut on runtime health, as the shell already is The POSIX half of this loop was closed and the Windows half was not. setup.ps1 reaches "already built" through an elseif on Test-PathQuiet $LlamaServerBin, so a Windows repair that fell through to the source stage kept a tree whose ggml-base.dll had been quarantined, returned it byte for byte identical, and reported success. Desktop preflight grades that same tree on every launch, so it offered the same repair forever. Test-LlamaTreeStillHealthy makes the same --check-existing-install call setup.sh makes, through Invoke-LlamaHelper, so there is one definition of healthy rather than two that can disagree. It answers healthy on any failure to ask: a helper that cannot run must not turn into a multi-gigabyte source build on a machine whose only fault was an unreadable tree. The reason this survived is in the convergence model's own comment, which said setup.ps1 had no reuse step and that a Windows update reaching the source stage always builds. That was wrong, and the model therefore never exercised the Windows branch. It now models both shells, and reverting the gate in the model fails every Windows cell of test_no_damaged_tree_survives_the_shell_rebuild_skip, which is what it should have done all along. A model cannot catch a shell losing its gate, so test_both_shells_gate_their_reuse_shortcut_on_the_same_check reads both scripts and asserts the check sits on the shortcut itself rather than somewhere else in the file. tests/studio/install: 6007 passed, 6 skipped. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Reject a library renamed in place instead of letting it satisfy its own group * Grade the root entrypoint, and plan the Windows rebuild the health gate forces * Grade the studio root the desktop scrubbed, and fix the Windows fixture * Reject empty payload files, and stop grading a runtime no repair can reach * Give the entrypoint check one owner, and cache only what the probe read * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Close the repair loop between the launch check and the update Found by simulating the cycle a user walks rather than by testing the new check on its own, and reproduced before being fixed. installed_runtime_health rejects a root llama-server or llama-quantize that lost its execute bit, so the desktop marks the install stale and offers a repair. existing_install_current_without_plan is the shortcut install_prebuilt returns on before reinstalling anything, and it checked os.access(X_OK) under build/bin ONLY, while _find_llama_server_binary reaches the install root's copy FIRST. So the repair ran, changed nothing, and the next launch was stale again. Measured, with an undamaged install accepted as the control: root llama-server -x launch health broken, shortcut True, full False root llama-quantize -x launch health broken, shortcut True, full False build/bin copy -x launch health broken, shortcut False, full False _damaged_entrypoint now answers for the shortcut too, which is what its own docstring already asks for: one owner, so a tree one check rejects and another keeps cannot be repaired by changing nothing. It is four stat plus os.access calls and spawns nothing, so the shortcut stays a shortcut. test_the_execute_bit_is_checked_on_the_runtime_copies_only pinned the old boundary, so it is rewritten as test_the_execute_bit_is_checked_on_every_entrypoint, keeping the visual server as the control that stops the fix becoming "reject everything". tests/studio/install plus test_llama_cpp_path_settings.py: 8787 passed, 65 skipped. * Require the split entrypoint dylibs on macOS The impl split that gave Windows llama-server-impl.dll and Linux libllama-server-impl.so gives macOS libllama-server-impl.dylib and libllama-quantize-impl.dylib, and the macOS branch of runtime_payload_health_groups was the one that did not ask for them. Quarantining either left every library group satisfied and both execute-bit checks passing while dyld could not start the entrypoint, so installed_runtime_health answered Ready on the platform this probe exists for. Verified against the real bundles rather than assumed: both llama-b10840-mix-d5c17a0 and llama-b11007-mix-3e83366 macos-arm64 ship the two, unversioned, beside the entrypoints. _PREBUILT_TREE_EVIDENCE already reads libllama-server-impl*.dylib as proof of a prebuilt macOS tree. _macos_split_entrypoint_groups delegates the gate to its Linux counterpart rather than repeating it, so the source labels and the b9283 floor cannot drift. The three fixtures that claimed to list a real macOS bundle were short of these two files and now carry them, and the matrix states the requirement independently with the matching pre-split pruning. New test with the loop-direction control: a pre-split upstream b6099 bundle never shipped them and stays healthy. * Shorten the macOS split docstring Comment-only; the AST gate reports install_llama_prebuilt.py code-unchanged. * Grade the macOS fixture against a real bundle, and stop a test carrying one machine's path Merging main up brought in test_macos_loader_probe_record.py from #11237, whose install fixture writes three dylibs. This branch replaced main's three loose macOS globs with one group per library, so the fixture is now thinner than any install the code grades and 18 of its 26 tests failed on the merge. The tightening is the right side of that: on the shipped llama-b11030-mix-5ff778e-bin-macos-arm64 bundle, quarantining libggml-base leaves main answering healthy, because libggml-blas, libggml-metal and libggml-rpc still satisfy libggml*.dylib, and answers llama_runtime_payload_incomplete here. The fixture now writes the libraries that bundle actually carries, including the two impl dylibs the unparseable release tag requires under the same gate Windows has always used. test_llama_runtime_install_history.py defaulted its asset directory to one machine's workspace path. The default is now under the user's own cache, and the presence check no longer lets a PermissionError through: the file promises to skip when an artifact is not available, and an unreadable directory failed 24 tests instead. tests/studio/install: 8796 passed, 76 skipped. * Name the folder the antivirus advice is asking the user to allow The message told the user to allow "the folder" without saying which one, and finding it is the whole work of acting on that sentence. It now names the default root, spelled the way the platform spells it, off the same navigator test the roaming-profile hint beside it already uses. Hedged with "usually" rather than asserted: UNSLOTH_LLAMA_CPP_PATH can move the root and preflight does not carry the path, so a definite statement would be wrong for anyone who set it. No navigator at all keeps the POSIX spelling rather than dropping the advice. Three tests: every reason names a folder, Windows gets the Windows spelling and no POSIX one (a path a user cannot paste into an exclusion list is not advice), and a missing navigator still answers. 15 of 15 in the file. * Grade a root entrypoint only when discovery would start it The root check was stricter than the resolver it was modelling. It rejected any root copy that existed and was not runnable, but resolve_llama_server_binary returns the first USABLE candidate, and _usable_binary is is_file() plus os.access(X_OK) off Windows. A root wrapper whose execute bit is gone is walked past, build/bin runs, and the tree is fine; grading it damaged sent a working runtime through a repair, and where repair is unavailable it blocked the launch. Reproduced against the real resolver on a Linux tree before changing anything: root wrapper executable backend picks <root>/llama-server health (True, '') root wrapper chmod 0644 backend picks <root>/build/bin/llama-server health (False, binaries_missing) root wrapper executable but empty backend picks <root>/llama-server health (False, binaries_missing) The middle row is the false positive. The third is the one that matters and is kept: an empty wrapper keeps is_file() and its execute bit, so discovery does select it and the exec dies on ENOEXEC. _discovery_would_select mirrors _usable_binary, so the root copy is graded only when the resolver would stop there. Windows keeps the current behaviour, since _usable_binary does not consult the execute bit there and an empty .exe is still what gets selected. test_a_rotten_root_entrypoint_is_not_saved_by_a_healthy_build_bin asserted the old behaviour on the strength of "the finder reaches the root first", which is true but incomplete: it reaches it and skips it. Replaced with the empty-wrapper case, plus a test that a 0644 wrapper and a directory of that name are both walked past, the Windows empty-exe case, and a parity test against the backend's own _usable_binary so the two cannot drift. tests/studio/install/test_installed_runtime_health.py: 53 passed. * Scope the root-entrypoint relaxation to the launch verdict The first cut changed _damaged_entrypoint for all three callers, which also relaxed the installer's keep decision and the release-reuse check. Those ask a different question: whether this tree can be reused as it stands, and a rotten root wrapper is worth replacing, which is what a reinstall is for. selected_root_only is the launch probe's rule alone. The launch verdict follows the resolver, so it walks past a wrapper with no execute bit and does not mark a working runtime stale; the keep decision stays as it was. The direction is the safe one: the launch verdict is never stricter than the repair that answers it, which is the property that stops a tree being repaired by changing nothing and rejected again on the next launch. test_a_damaged_root_entrypoint_stops_the_release_being_reused keeps its point, that the keep decision must reject where the shortcut once kept, and gains the empty-wrapper row, which is the damage the resolver does select and run. tests/studio/install: 8970 passed, 76 skipped, 1 failed, and that one (test_an_unserved_preferred_arch_source_builds_rather_than_using_the_igpus_bundle) fails identically on a clean origin/main worktree. * Grade the runtime a master root install actually uses Under UNSLOTH_HOME the runtimes sit beside studio/, and the CLI exports <master>/llama.cpp while UNSLOTH_STUDIO_HOME stays <master>/studio. The comparison path dropped the override and called default_managed_llama_dir(), which reads only the studio home, so it answered <master>/studio/llama.cpp: the installer's own export did not match, the tree in use graded as a user pin, and master root installs never reached the health check. The rule now lives in _master_root_llama_dir() and both the export and the grading read it, so there is one answer rather than two that can drift. * Fingerprint the runtime a master root install actually loads The Rust half of the same split: inferred_studio_llama_root() answered <master>/studio/llama.cpp for an install recorded under UNSLOTH_HOME, while the CLI grades and the backend loads <master>/llama.cpp. One healthy cached result then survived quarantine in the runtime in use, so preflight kept returning Ready and the failure only appeared at model load, with no repair offered. It now reads share/.unsloth-master-root under the same containment rule the CLI applies (the recorded root must exist and contain this Studio tree) and fingerprints the sibling runtime. Tests cover the note, a note naming somebody else's root, an empty one and one naming a directory that is not there. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <unslothshared@gmail.com> Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com> Co-authored-by: danielhanchen <unslothai@gmail.com> Co-authored-by: danielhanchen <moonshotaisubstack@gmail.com>
360 lines
15 KiB
Python
360 lines
15 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Persist and validate the llama.cpp directory selected in Unsloth settings."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import threading
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Iterable, Iterator, Optional
|
|
|
|
CUSTOM_LLAMA_CPP_PATH_SETTING_KEY = "custom_llama_cpp_path"
|
|
MAX_CUSTOM_LLAMA_CPP_PATH_LENGTH = 32767
|
|
MANAGED_LLAMA_CPP_PATH_MARKER = "UNSLOTH_STUDIO_MANAGED_LLAMA_CPP_PATH"
|
|
|
|
_settings_lock = threading.RLock()
|
|
_path_revision = 0
|
|
|
|
|
|
def expanded_user_path(value: Path | str) -> Path:
|
|
"""``~`` and ``~name`` expanded, never raising on a name that does not resolve.
|
|
|
|
``Path.expanduser()`` raises ``RuntimeError`` when the home cannot be
|
|
determined, which includes every unknown named user: ``~deleted-user/llama.cpp``
|
|
left in a service unit or a ``.env`` after an account rename took runtime
|
|
discovery down with it, so a model operation reported the expansion error
|
|
rather than falling through to another runtime or saying none was found.
|
|
|
|
``os.path.expanduser`` is the reader that does not raise: it hands an
|
|
unresolvable name straight back, which is also what the desktop's pinning does
|
|
with one, so the two halves of a launch still agree about the value. An
|
|
unexpandable override then reaches the search as an ordinary path, finds
|
|
nothing, and the documented order continues.
|
|
"""
|
|
return Path(os.path.expanduser(str(value)))
|
|
|
|
|
|
def mark_managed_llama_cpp_path(directory: Path | str) -> bool:
|
|
"""Mark Unsloth's inherited install path without hiding a real env override."""
|
|
configured = os.environ.get("UNSLOTH_LLAMA_CPP_PATH", "").strip()
|
|
if not configured:
|
|
os.environ.pop(MANAGED_LLAMA_CPP_PATH_MARKER, None)
|
|
return False
|
|
try:
|
|
managed = expanded_user_path(directory).resolve(strict = False)
|
|
inherited = expanded_user_path(configured).resolve(strict = False)
|
|
is_managed = inherited == managed
|
|
except (OSError, RuntimeError, ValueError):
|
|
is_managed = False
|
|
if is_managed:
|
|
os.environ[MANAGED_LLAMA_CPP_PATH_MARKER] = "1"
|
|
else:
|
|
os.environ.pop(MANAGED_LLAMA_CPP_PATH_MARKER, None)
|
|
return is_managed
|
|
|
|
|
|
@contextmanager
|
|
def llama_cpp_path_selection_guard() -> Iterator[None]:
|
|
"""Serialize a runtime path snapshot with a settings write.
|
|
|
|
Model loads and UI saves share this lock so reload status sees one snapshot.
|
|
"""
|
|
with _settings_lock:
|
|
yield
|
|
|
|
|
|
def llama_server_binary_name(platform: Optional[str] = None) -> str:
|
|
return "llama-server.exe" if (platform or sys.platform) == "win32" else "llama-server"
|
|
|
|
|
|
def llama_server_candidates(
|
|
directory: Path | str, *, platform: Optional[str] = None
|
|
) -> tuple[Path, ...]:
|
|
"""Supported llama.cpp build layouts, in the runtime's search order."""
|
|
root = Path(directory)
|
|
binary_name = llama_server_binary_name(platform)
|
|
windows = (platform or sys.platform) == "win32"
|
|
candidates = [root / binary_name]
|
|
# build/ first, then the per-backend build dirs a source checkout keeps side by side (#5941).
|
|
for build in ("build", "build-cuda", "build-hip", "build-rocm", "build-vulkan"):
|
|
candidates.append(root / build / "bin" / binary_name)
|
|
if windows:
|
|
candidates.append(root / build / "bin" / "Release" / binary_name)
|
|
return tuple(candidates)
|
|
|
|
|
|
def _usable_binary(path: Path, *, platform: Optional[str] = None) -> bool:
|
|
try:
|
|
if not path.is_file():
|
|
return False
|
|
except OSError:
|
|
return False
|
|
return (platform or sys.platform) == "win32" or os.access(path, os.X_OK)
|
|
|
|
|
|
_GPU_BACKEND_LIB_RE = re.compile(
|
|
r"^(?:lib)?ggml-(cann|cuda|hip|metal|musa|opencl|sycl|virtgpu|vulkan)"
|
|
r"(?:\.dll|\.so(?:\.\d+)*|(?:\.\d+)*\.dylib)$"
|
|
)
|
|
_CPU_BACKEND_LIB_RE = re.compile(r"^(?:lib)?ggml-(?:cpu|base)(?:[-.]|$)")
|
|
# Backends bound to one vendor; vulkan, opencl and virtgpu run on any GPU.
|
|
_BACKEND_VENDOR = {
|
|
"cuda": "nvidia",
|
|
"hip": "amd",
|
|
"sycl": "intel",
|
|
"metal": "apple",
|
|
"musa": "mthreads",
|
|
"cann": "huawei",
|
|
}
|
|
_DRM_VENDOR = {"0x10de": "nvidia", "0x1002": "amd", "0x8086": "intel"}
|
|
_WINDOWS_VENDOR_DLLS = {
|
|
"nvidia": ("nvcuda.dll",),
|
|
"amd": ("amdhip64*.dll", "amdocl64.dll"),
|
|
"intel": ("ze_intel_gpu64.dll", "igdrcl64.dll"),
|
|
}
|
|
_DRM_ROOT = "/sys/class/drm"
|
|
# WSL2 reaches the GPU through /dev/dxg (no DRM card, no /dev/kfd or /dev/nvidiactl): the
|
|
# vendor is the runtime that drives it, librocdxg for AMD and the WSL libcuda for NVIDIA.
|
|
_WSL_ROCM_LIB_DIRS = ("/opt/rocm/lib", "/opt/rocm/lib64")
|
|
_WSL_CUDA_LIB_DIR = "/usr/lib/wsl/lib"
|
|
_HOST: Any = object() # prefer_gpu_capable's default: read the vendors from this host
|
|
|
|
|
|
def binary_gpu_backends(binary: Path | str) -> Optional[set[str]]:
|
|
"""GPU backend libraries beside the binary; empty for a split build with only CPU
|
|
libraries, None when the layout says nothing (a static build or a wrapper)."""
|
|
try:
|
|
names = [p.name for p in Path(binary).resolve().parent.iterdir() if p.is_file()]
|
|
except OSError:
|
|
return None
|
|
gpu = {m.group(1) for m in map(_GPU_BACKEND_LIB_RE.match, names) if m}
|
|
if gpu or any(_CPU_BACKEND_LIB_RE.match(name) for name in names):
|
|
return gpu
|
|
return None
|
|
|
|
|
|
def binary_gpu_verdict(binary: Path | str) -> str:
|
|
"""``gpu``, ``cpu`` (a split-library build with no GPU backend beside it) or ``unknown``."""
|
|
backends = binary_gpu_backends(binary)
|
|
return "unknown" if backends is None else ("gpu" if backends else "cpu")
|
|
|
|
|
|
def _mask_hides_all(*names: str) -> bool:
|
|
"""Whether the first set mask among names (their precedence order) hides every device."""
|
|
mask = next((os.environ[n] for n in names if n in os.environ), "x")
|
|
return mask.strip() in ("", "-1")
|
|
|
|
|
|
def _amd_mask_hides_all() -> bool:
|
|
"""HIP_ and ROCR_VISIBLE_DEVICES stack (ROCR filters the agents HIP then indexes), so an
|
|
empty one at either layer hides everything; CUDA_VISIBLE_DEVICES stands in only when
|
|
neither is set (_active_gpu_visibility_mask)."""
|
|
if any(n in os.environ for n in ("HIP_VISIBLE_DEVICES", "ROCR_VISIBLE_DEVICES")):
|
|
return _mask_hides_all("HIP_VISIBLE_DEVICES") or _mask_hides_all("ROCR_VISIBLE_DEVICES")
|
|
return _mask_hides_all("CUDA_VISIBLE_DEVICES")
|
|
|
|
|
|
def host_gpu_vendors() -> Optional[set[str]]:
|
|
"""GPU vendors this process can reach: DRM sysfs vendors whose device node is present and
|
|
whose visibility mask is not empty on Linux, the vendors' driver DLLs on Windows. None when
|
|
nothing was detected at all (an unknown host filters nothing); an empty set when GPUs were
|
|
detected but none is reachable, so only vendor-agnostic backends fit."""
|
|
vendors: set[str] = set()
|
|
if sys.platform == "darwin":
|
|
return {"apple"}
|
|
if sys.platform == "win32":
|
|
system32 = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32"
|
|
for vendor, patterns in _WINDOWS_VENDOR_DLLS.items():
|
|
if any(next(system32.glob(pattern), None) for pattern in patterns):
|
|
vendors.add(vendor)
|
|
else:
|
|
for vendor_file in Path(_DRM_ROOT).glob("card*/device/vendor"):
|
|
try:
|
|
vendor = _DRM_VENDOR.get(vendor_file.read_text(encoding = "utf-8").strip().lower())
|
|
except OSError:
|
|
continue
|
|
if vendor:
|
|
vendors.add(vendor)
|
|
if os.path.isdir("/proc/driver/nvidia"):
|
|
vendors.add("nvidia")
|
|
wsl = os.path.exists("/dev/dxg")
|
|
wsl_amd = wsl and any(
|
|
os.path.exists(os.path.join(d, n))
|
|
for d in _WSL_ROCM_LIB_DIRS
|
|
for n in ("librocdxg.so", "librocdxg.so.1")
|
|
)
|
|
wsl_nvidia = wsl and any(Path(_WSL_CUDA_LIB_DIR).glob("libcuda.so*"))
|
|
vendors |= {v for v, on in (("amd", wsl_amd), ("nvidia", wsl_nvidia)) if on}
|
|
detected = set(vendors)
|
|
# A container that exposes only one vendor's device nodes, or a mask hiding a vendor,
|
|
# must not make its backend look runnable on a hybrid box.
|
|
if "nvidia" in vendors and (
|
|
not (os.path.exists("/dev/nvidiactl") or wsl_nvidia)
|
|
or _mask_hides_all("CUDA_VISIBLE_DEVICES")
|
|
):
|
|
vendors.discard("nvidia")
|
|
if "amd" in vendors and (
|
|
not (os.path.exists("/dev/kfd") or wsl_amd) or _amd_mask_hides_all()
|
|
):
|
|
vendors.discard("amd")
|
|
return vendors if detected else None
|
|
return vendors or None
|
|
|
|
|
|
def _fits_host(backends: set[str], vendors: Optional[set[str]]) -> bool:
|
|
if vendors is None:
|
|
return True
|
|
return any(_BACKEND_VENDOR.get(b) is None or _BACKEND_VENDOR[b] in vendors for b in backends)
|
|
|
|
|
|
def prefer_gpu_capable(
|
|
candidates: Iterable[Path],
|
|
usable: Callable[[Path], bool],
|
|
vendors: Any = _HOST,
|
|
) -> list[Path]:
|
|
"""Search order, except that a first hit proven CPU-only, or proven built for a GPU vendor
|
|
this host lacks, yields to a later usable build shipping a GPU backend this host's vendor
|
|
runs (#5941: a CPU build/ beside build-cuda/; a stale build-cuda/ must not shadow
|
|
build-hip/ on an AMD box, whether or not a build/ precedes it). Unknown layouts stay put."""
|
|
ordered = list(candidates)
|
|
first = next((c for c in ordered if usable(c)), None)
|
|
if first is None:
|
|
return ordered
|
|
verdict = binary_gpu_verdict(first)
|
|
if verdict == "unknown":
|
|
return ordered
|
|
if vendors is _HOST:
|
|
vendors = host_gpu_vendors()
|
|
if verdict == "gpu" and _fits_host(binary_gpu_backends(first), vendors):
|
|
return ordered
|
|
for candidate in ordered:
|
|
if candidate == first or not usable(candidate):
|
|
continue
|
|
backends = binary_gpu_backends(candidate)
|
|
if backends and _fits_host(backends, vendors):
|
|
return [candidate] + [c for c in ordered if c != candidate]
|
|
return ordered
|
|
|
|
|
|
def resolve_llama_server_binary(
|
|
directory: Path | str, *, platform: Optional[str] = None
|
|
) -> Optional[Path]:
|
|
"""Return the first executable llama-server in a supported layout, GPU-capable preferred."""
|
|
usable = lambda candidate: _usable_binary(candidate, platform = platform) # noqa: E731
|
|
ordered = prefer_gpu_capable(llama_server_candidates(directory, platform = platform), usable)
|
|
return next((candidate for candidate in ordered if usable(candidate)), None)
|
|
|
|
|
|
def get_stored_custom_llama_cpp_path() -> Optional[Path]:
|
|
"""The Unsloth-selected directory, or ``None`` when automatic discovery is active."""
|
|
try:
|
|
from storage.studio_db import get_app_setting
|
|
from utils.account_context import OWNER, run_as
|
|
value = run_as(OWNER, get_app_setting, CUSTOM_LLAMA_CPP_PATH_SETTING_KEY, None)
|
|
except Exception:
|
|
# A settings DB problem must not take the bundled runtime down with it.
|
|
return None
|
|
if not isinstance(value, str):
|
|
return None
|
|
value = value.strip()
|
|
if not value or len(value) > MAX_CUSTOM_LLAMA_CPP_PATH_LENGTH:
|
|
return None
|
|
return expanded_user_path(value)
|
|
|
|
|
|
def _environment_override() -> tuple[Optional[str], Optional[str], bool]:
|
|
"""``(path, variable, direct_binary)`` for the existing environment pins."""
|
|
direct = os.environ.get("LLAMA_SERVER_PATH", "").strip()
|
|
if direct:
|
|
return direct, "LLAMA_SERVER_PATH", True
|
|
directory = os.environ.get("UNSLOTH_LLAMA_CPP_PATH", "").strip()
|
|
if directory and os.environ.get(MANAGED_LLAMA_CPP_PATH_MARKER) != "1":
|
|
return directory, "UNSLOTH_LLAMA_CPP_PATH", False
|
|
return None, None, False
|
|
|
|
|
|
def custom_llama_cpp_path_source() -> str:
|
|
"""The active custom-path authority: environment, studio, or default."""
|
|
env_path, _variable, _direct = _environment_override()
|
|
if env_path is not None:
|
|
return "environment"
|
|
if get_stored_custom_llama_cpp_path() is not None:
|
|
return "studio"
|
|
return "default"
|
|
|
|
|
|
def _canonical_directory(value: str) -> Path:
|
|
raw = value.strip()
|
|
if not raw:
|
|
raise ValueError("Choose a llama.cpp folder or use the bundled runtime.")
|
|
if len(raw) > MAX_CUSTOM_LLAMA_CPP_PATH_LENGTH:
|
|
raise ValueError("The llama.cpp folder path is too long.")
|
|
try:
|
|
directory = expanded_user_path(raw).resolve(strict = True)
|
|
except (OSError, RuntimeError, ValueError) as exc:
|
|
raise ValueError("The llama.cpp folder does not exist or cannot be accessed.") from exc
|
|
if not directory.is_dir():
|
|
raise ValueError("The custom llama.cpp path must be a folder.")
|
|
if resolve_llama_server_binary(directory) is None:
|
|
binary_name = llama_server_binary_name()
|
|
raise ValueError(
|
|
f"No executable {binary_name} was found in that folder or its build/bin directory."
|
|
)
|
|
return directory
|
|
|
|
|
|
def set_custom_llama_cpp_path(value: Optional[str]) -> Optional[Path]:
|
|
"""Store a validated directory. ``None`` restores automatic discovery."""
|
|
global _path_revision
|
|
env_path, variable, _direct = _environment_override()
|
|
if env_path is not None:
|
|
raise RuntimeError(f"The llama.cpp path is managed by the {variable} environment variable.")
|
|
directory = _canonical_directory(value) if value is not None else None
|
|
with _settings_lock:
|
|
from storage.studio_db import upsert_app_settings
|
|
upsert_app_settings(
|
|
{CUSTOM_LLAMA_CPP_PATH_SETTING_KEY: (str(directory) if directory is not None else None)}
|
|
)
|
|
_path_revision += 1
|
|
return directory
|
|
|
|
|
|
def custom_llama_cpp_path_revision() -> int:
|
|
"""In-process revision used to retire sidecars launched before a path save."""
|
|
with _settings_lock:
|
|
return _path_revision
|
|
|
|
|
|
def custom_llama_cpp_path_status() -> dict:
|
|
"""UI payload describing the effective custom-path selection."""
|
|
env_path, variable, direct_binary = _environment_override()
|
|
source = "default"
|
|
path: Optional[Path] = None
|
|
binary: Optional[Path] = None
|
|
|
|
if env_path is not None:
|
|
source = "environment"
|
|
path = expanded_user_path(env_path)
|
|
if direct_binary:
|
|
binary = path if _usable_binary(path) else None
|
|
else:
|
|
binary = resolve_llama_server_binary(path)
|
|
else:
|
|
path = get_stored_custom_llama_cpp_path()
|
|
if path is not None:
|
|
source = "studio"
|
|
binary = resolve_llama_server_binary(path)
|
|
|
|
return {
|
|
"path": str(path) if path is not None else None,
|
|
"source": source,
|
|
"editable": source != "environment",
|
|
"available": source == "default" or binary is not None,
|
|
"resolved_binary": str(binary) if binary is not None else None,
|
|
"environment_variable": variable,
|
|
}
|