mirror of
https://github.com/unslothai/unsloth.git
synced 2026-07-09 15:58:41 +00:00
Fix Hermes install hint on Windows (#6903)
* fix: use Windows Hermes installer from unsloth start * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Skip the Hermes setup wizard during unattended start-install unsloth start hermes auto-installs Hermes and then writes its own session-scoped Hermes config. The install commands, as written, drop into the installer's interactive setup wizard (hermes setup), which prompts for global API keys and model choice and points the user at a different global provider than the one Unsloth just configured, blocking the launch. Pass the installer's skip flag on both platforms: the PowerShell scriptblock form with -SkipSetup, and bash -s -- --skip-setup for the piped POSIX installer. * Refresh PATH from the registry after a Windows agent install A Windows installer persists the agent's directory to the User/Machine PATH in the registry and updates only its own process, so the current process keeps a stale PATH until it restarts (the installers print 'restart your terminal'). The post-install shutil.which then misses the just-installed agent and unsloth start fails with 'installed but isn't on PATH yet', forcing a re-run in a new shell. Merge the registry PATH hives back into the process before re-resolving so a freshly installed agent launches in the same invocation. No-op off Windows and on any read error; only ever augments PATH. * Fix/adjust PATH refresh for PR #6903 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <danielhanchen@gmail.com> Co-authored-by: wasimysaid <112766706+wasimysaid@users.noreply.github.com> Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com>
This commit is contained in:
parent
393d7e9c2b
commit
baacbd025d
2 changed files with 165 additions and 4 deletions
|
|
@ -44,6 +44,19 @@ _CODEX_PROFILE = "unsloth_api"
|
|||
_CODEX_ENV_KEY = "UNSLOTH_STUDIO_AUTH_TOKEN"
|
||||
_HERMES_ENV_KEY = "UNSLOTH_API_KEY"
|
||||
_HERMES_PROVIDER = "unsloth"
|
||||
# Skip the installer's interactive setup wizard: `unsloth start hermes` runs
|
||||
# this hint unattended and then writes its own session-scoped Hermes config, so
|
||||
# the wizard's global API-key/model prompts would block the launch and point the
|
||||
# user at a different (global) provider than the one Unsloth just configured.
|
||||
# Both installers expose a skip flag: `-SkipSetup` (PowerShell) and
|
||||
# `--skip-setup` (POSIX; passed to the piped script via `bash -s --`).
|
||||
_HERMES_WINDOWS_INSTALL_HINT = (
|
||||
"& ([scriptblock]::Create((irm https://hermes-agent.nousresearch.com/install.ps1))) -SkipSetup"
|
||||
)
|
||||
_HERMES_POSIX_INSTALL_HINT = (
|
||||
"curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent"
|
||||
"/main/scripts/install.sh | bash -s -- --skip-setup"
|
||||
)
|
||||
# Hermes refuses to initialize when the model window is under 64,000 tokens; its
|
||||
# error message points at the model.context_length / auxiliary.compression
|
||||
# overrides in config.yaml. write_hermes_config claims this value for smaller
|
||||
|
|
@ -138,6 +151,10 @@ def _yolo_command_flags(agent: str, yolo: bool) -> list:
|
|||
return _YOLO_COMMAND_FLAGS.get(agent, []) if yolo else []
|
||||
|
||||
|
||||
def _hermes_install_hint() -> str:
|
||||
return _HERMES_WINDOWS_INSTALL_HINT if os.name == "nt" else _HERMES_POSIX_INSTALL_HINT
|
||||
|
||||
|
||||
class LoadOptions(NamedTuple):
|
||||
"""Model-load knobs forwarded to /api/inference/load when --model triggers a load."""
|
||||
|
||||
|
|
@ -848,6 +865,54 @@ def _print_env(
|
|||
typer.echo(" ".join((*inline, shlex.join(command))))
|
||||
|
||||
|
||||
def _refresh_windows_path() -> None:
|
||||
# Merge Windows registry PATH hives after the current process PATH so a
|
||||
# freshly installed agent is visible without changing existing precedence.
|
||||
if os.name != "nt":
|
||||
return
|
||||
try:
|
||||
import winreg
|
||||
except Exception:
|
||||
return
|
||||
|
||||
entries = []
|
||||
seen = set()
|
||||
|
||||
def add_path(value: str) -> bool:
|
||||
added = False
|
||||
for entry in str(value).split(os.pathsep):
|
||||
entry = entry.strip()
|
||||
if not entry:
|
||||
continue
|
||||
key = os.path.normcase(entry).casefold()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
entries.append(entry)
|
||||
added = True
|
||||
return added
|
||||
|
||||
add_path(os.environ.get("PATH", ""))
|
||||
added_registry = False
|
||||
hives = (
|
||||
(winreg.HKEY_CURRENT_USER, "Environment"),
|
||||
(
|
||||
winreg.HKEY_LOCAL_MACHINE,
|
||||
r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
|
||||
),
|
||||
)
|
||||
for root, sub in hives:
|
||||
try:
|
||||
with winreg.OpenKey(root, sub) as key:
|
||||
value, _ = winreg.QueryValueEx(key, "Path")
|
||||
except OSError:
|
||||
continue
|
||||
if value:
|
||||
added_registry = add_path(os.path.expandvars(str(value))) or added_registry
|
||||
if added_registry:
|
||||
os.environ["PATH"] = os.pathsep.join(entries)
|
||||
|
||||
|
||||
def _install_agent(name: str, install_hint: str) -> Optional[str]:
|
||||
# Missing agent under --launch: offer to run its documented install command, then
|
||||
# re-resolve it on PATH. Consent-based (we never auto-run a remote install script
|
||||
|
|
@ -866,6 +931,9 @@ def _install_agent(name: str, install_hint: str) -> Optional[str]:
|
|||
install_command = ["/bin/sh", "-c", install_hint]
|
||||
if subprocess.run(install_command).returncode != 0:
|
||||
_fail(f"Install command failed. Run it yourself, then re-run: {install_hint}")
|
||||
# The installer just wrote PATH to the registry (Windows); pull it into this
|
||||
# process so the freshly installed agent resolves without a shell restart.
|
||||
_refresh_windows_path()
|
||||
executable = shutil.which(name)
|
||||
if executable is None:
|
||||
_fail(
|
||||
|
|
@ -1536,10 +1604,7 @@ def hermes(
|
|||
launch = launch,
|
||||
)
|
||||
command = ["hermes", *_yolo_command_flags("hermes", yolo), *ctx.args]
|
||||
install_hint = (
|
||||
"curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent"
|
||||
"/main/scripts/install.sh | bash"
|
||||
)
|
||||
install_hint = _hermes_install_hint()
|
||||
with _session_config("hermes", launch) as home:
|
||||
# HERMES_HOME relocates hermes' whole home dir (config.yaml, sessions, state)
|
||||
# like CODEX_HOME, so the user's ~/.hermes is left untouched for the session.
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ def test_claude_flags_detected_when_version_not_first_token(monkeypatch):
|
|||
|
||||
def test_install_agent_prompts_then_installs(monkeypatch):
|
||||
# TTY + yes: run the documented install command, then re-resolve the now-present binary.
|
||||
monkeypatch.setattr(start.os, "name", "posix")
|
||||
monkeypatch.setattr(start.sys, "stdin", SimpleNamespace(isatty = lambda: True))
|
||||
monkeypatch.setattr(start.typer, "confirm", lambda *a, **k: True)
|
||||
ran = []
|
||||
|
|
@ -108,6 +109,101 @@ def test_install_agent_prompts_then_installs(monkeypatch):
|
|||
assert ran == [["/bin/sh", "-c", "npm install -g @openai/codex"]]
|
||||
|
||||
|
||||
def test_install_agent_uses_powershell_on_windows(monkeypatch):
|
||||
monkeypatch.setattr(start.os, "name", "nt")
|
||||
monkeypatch.setattr(start.sys, "stdin", SimpleNamespace(isatty = lambda: True))
|
||||
monkeypatch.setattr(start.typer, "confirm", lambda *a, **k: True)
|
||||
ran = []
|
||||
monkeypatch.setattr(
|
||||
start.subprocess,
|
||||
"run",
|
||||
lambda command, *a, **k: ran.append(command) or SimpleNamespace(returncode = 0),
|
||||
)
|
||||
monkeypatch.setattr(start.shutil, "which", lambda _: r"C:\Users\samle\bin\hermes.exe")
|
||||
|
||||
install_hint = "& ([scriptblock]::Create((irm https://x/install.ps1))) -SkipSetup"
|
||||
executable = start._install_agent("hermes", install_hint)
|
||||
|
||||
assert executable == r"C:\Users\samle\bin\hermes.exe"
|
||||
assert ran == [["powershell", "-NoProfile", "-Command", install_hint]]
|
||||
|
||||
|
||||
def test_hermes_install_hint_is_windows_native_on_windows(monkeypatch):
|
||||
monkeypatch.setattr(start.os, "name", "nt")
|
||||
|
||||
# Scriptblock form so `-SkipSetup` reaches the installer and the interactive
|
||||
# setup wizard is skipped during the unattended `unsloth start hermes` run.
|
||||
assert start._hermes_install_hint() == (
|
||||
"& ([scriptblock]::Create((irm https://hermes-agent.nousresearch.com/install.ps1)))"
|
||||
" -SkipSetup"
|
||||
)
|
||||
|
||||
|
||||
def test_hermes_install_hint_is_bash_on_posix(monkeypatch):
|
||||
monkeypatch.setattr(start.os, "name", "posix")
|
||||
|
||||
# `bash -s -- --skip-setup` forwards the skip flag to the piped installer.
|
||||
assert start._hermes_install_hint() == (
|
||||
"curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent"
|
||||
"/main/scripts/install.sh | bash -s -- --skip-setup"
|
||||
)
|
||||
|
||||
|
||||
def test_refresh_windows_path_noop_off_windows(monkeypatch):
|
||||
monkeypatch.setattr(start.os, "name", "posix")
|
||||
before = os.environ.get("PATH", "")
|
||||
monkeypatch.setenv("PATH", before)
|
||||
start._refresh_windows_path()
|
||||
assert os.environ.get("PATH", "") == before
|
||||
|
||||
|
||||
def test_refresh_windows_path_merges_registry_hives(monkeypatch):
|
||||
# Fake Windows registry PATH values written after this process started.
|
||||
hkcu, hklm = object(), object()
|
||||
reg = {
|
||||
(hkcu, "Environment"): r"C:\existing;C:\Users\me\hermes\bin",
|
||||
(
|
||||
hklm,
|
||||
r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
|
||||
): r"C:\Windows\System32",
|
||||
}
|
||||
|
||||
class _Key:
|
||||
def __init__(self, value):
|
||||
self._value = value
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
def open_key(root, sub):
|
||||
if (root, sub) in reg:
|
||||
return _Key(reg[(root, sub)])
|
||||
raise OSError("missing hive")
|
||||
|
||||
fake_winreg = SimpleNamespace(
|
||||
HKEY_CURRENT_USER = hkcu,
|
||||
HKEY_LOCAL_MACHINE = hklm,
|
||||
OpenKey = open_key,
|
||||
QueryValueEx = lambda key, name: (key._value, 1),
|
||||
)
|
||||
monkeypatch.setattr(start.os, "name", "nt")
|
||||
monkeypatch.setattr(start.os, "pathsep", ";")
|
||||
monkeypatch.setitem(sys.modules, "winreg", fake_winreg)
|
||||
monkeypatch.setenv("PATH", r"C:\custom;C:\existing")
|
||||
|
||||
start._refresh_windows_path()
|
||||
|
||||
assert os.environ["PATH"].split(";") == [
|
||||
r"C:\custom",
|
||||
r"C:\existing",
|
||||
r"C:\Users\me\hermes\bin",
|
||||
r"C:\Windows\System32",
|
||||
]
|
||||
|
||||
|
||||
def test_install_agent_declined_returns_none(monkeypatch):
|
||||
# TTY + no: never runs anything; caller falls back to the print-hint failure.
|
||||
monkeypatch.setattr(start.sys, "stdin", SimpleNamespace(isatty = lambda: True))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue