Update configuration and documentation for LLM support and local watchdog
- Added `ollama-models.yaml` to .gitignore and implemented logic to copy it in gpu-rent.ps1 and gpu-rent.sh. - Enhanced env.example to include new variables for LLM runtime options and local watchdog configuration. - Updated CLI commands to support LLM options during setup and execution, including new flags for Ollama and llama.cpp. - Improved documentation in cli.md and README.md to reflect changes in LLM integration and local watchdog functionality. - Adjusted architecture and decisions documentation to clarify the role of LLMs and local watchdog in the system.
This commit is contained in:
+1
-1
@@ -14,7 +14,7 @@ def test_help():
|
||||
def test_version():
|
||||
result = runner.invoke(app, ["version"])
|
||||
assert result.exit_code == 0
|
||||
assert "0.1.0" in result.stdout
|
||||
assert "0.2.0" in result.stdout
|
||||
|
||||
|
||||
def test_up_nyi_after_missing_env(monkeypatch, tmp_path):
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from gpu_rent.llm_runtime import (
|
||||
decide_runtime,
|
||||
normalize_runtime,
|
||||
parse_ollama_models,
|
||||
write_ollama_models_preset,
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_runtime():
|
||||
assert normalize_runtime(None) == "none"
|
||||
assert normalize_runtime("OLLAMA") == "ollama"
|
||||
assert normalize_runtime("llama-cpp") == "llamacpp"
|
||||
with pytest.raises(ValueError):
|
||||
normalize_runtime("foo")
|
||||
|
||||
|
||||
def test_decide_runtime_flags_win():
|
||||
assert (
|
||||
decide_runtime(flag=None, ollama_flag=True, llamacpp_flag=False, from_config="none")
|
||||
== "ollama"
|
||||
)
|
||||
assert (
|
||||
decide_runtime(flag="llamacpp", ollama_flag=False, llamacpp_flag=False, from_config="ollama")
|
||||
== "llamacpp"
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
decide_runtime(flag=None, ollama_flag=True, llamacpp_flag=True, from_config="none")
|
||||
|
||||
|
||||
def test_parse_ollama_models(tmp_path: Path):
|
||||
path = tmp_path / "m.yaml"
|
||||
path.write_text(
|
||||
"models:\n - name: huihui_ai/qwen2.5-abliterate:7b\n default: true\n - qwen2.5:3b\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
entries = parse_ollama_models(path)
|
||||
assert [e.name for e in entries] == [
|
||||
"huihui_ai/qwen2.5-abliterate:7b",
|
||||
"qwen2.5:3b",
|
||||
]
|
||||
assert entries[0].default is True
|
||||
|
||||
|
||||
def test_parse_empty_manifest(tmp_path: Path):
|
||||
path = tmp_path / "empty.yaml"
|
||||
path.write_text("models: []\n", encoding="utf-8")
|
||||
assert parse_ollama_models(path) == []
|
||||
assert parse_ollama_models(tmp_path / "missing.yaml") == []
|
||||
|
||||
|
||||
def test_write_preset(tmp_path: Path):
|
||||
path = tmp_path / "out.yaml"
|
||||
write_ollama_models_preset(path, "recommended")
|
||||
entries = parse_ollama_models(path)
|
||||
assert entries[0].name == "huihui_ai/qwen2.5-abliterate:7b"
|
||||
@@ -0,0 +1,87 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from gpu_rent.local_watchdog import LocalLease, decide_local_tick
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime(2026, 8, 21, 12, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def test_noop_when_not_installed():
|
||||
d = decide_local_tick(
|
||||
installed=False,
|
||||
has_server=True,
|
||||
lease=LocalLease(armed=True, heartbeat_at=_now().isoformat()),
|
||||
now=_now(),
|
||||
process_alive=False,
|
||||
grace_sec=600,
|
||||
)
|
||||
assert d.kind == "noop"
|
||||
|
||||
|
||||
def test_noop_when_detached():
|
||||
d = decide_local_tick(
|
||||
installed=True,
|
||||
has_server=True,
|
||||
lease=LocalLease(armed=False, detached=True, heartbeat_at=_now().isoformat()),
|
||||
now=_now(),
|
||||
process_alive=False,
|
||||
grace_sec=600,
|
||||
)
|
||||
assert d.kind == "noop"
|
||||
assert "detached" in d.detail
|
||||
|
||||
|
||||
def test_noop_while_pid_alive():
|
||||
d = decide_local_tick(
|
||||
installed=True,
|
||||
has_server=True,
|
||||
lease=LocalLease(
|
||||
armed=True,
|
||||
pid=1,
|
||||
heartbeat_at=(_now() - timedelta(hours=1)).isoformat(),
|
||||
),
|
||||
now=_now(),
|
||||
process_alive=True,
|
||||
grace_sec=600,
|
||||
)
|
||||
assert d.kind == "noop"
|
||||
|
||||
|
||||
def test_noop_inside_grace():
|
||||
hb = _now() - timedelta(minutes=5)
|
||||
d = decide_local_tick(
|
||||
installed=True,
|
||||
has_server=True,
|
||||
lease=LocalLease(armed=True, pid=999, heartbeat_at=hb.isoformat()),
|
||||
now=_now(),
|
||||
process_alive=False,
|
||||
grace_sec=600,
|
||||
)
|
||||
assert d.kind == "noop"
|
||||
assert "grace" in d.detail
|
||||
|
||||
|
||||
def test_stop_when_stale():
|
||||
hb = _now() - timedelta(minutes=20)
|
||||
d = decide_local_tick(
|
||||
installed=True,
|
||||
has_server=True,
|
||||
lease=LocalLease(armed=True, pid=999, heartbeat_at=hb.isoformat()),
|
||||
now=_now(),
|
||||
process_alive=False,
|
||||
grace_sec=600,
|
||||
)
|
||||
assert d.kind == "stop"
|
||||
|
||||
|
||||
def test_noop_without_lease():
|
||||
d = decide_local_tick(
|
||||
installed=True,
|
||||
has_server=True,
|
||||
lease=None,
|
||||
now=_now(),
|
||||
process_alive=False,
|
||||
grace_sec=600,
|
||||
)
|
||||
assert d.kind == "noop"
|
||||
@@ -57,3 +57,35 @@ def test_git_token_injection():
|
||||
assert strip_auth(with_token(url, "secret")) == url
|
||||
assert is_sha("a" * 40)
|
||||
assert not is_sha("main")
|
||||
|
||||
|
||||
def test_scrub_origin(tmp_path: Path, monkeypatch):
|
||||
from gpu_rent.remote import clone_ext
|
||||
|
||||
dest = tmp_path / "repo"
|
||||
dest.mkdir()
|
||||
(dest / ".git").mkdir()
|
||||
calls: list[list[str]] = []
|
||||
|
||||
def fake_out(argv, cwd=None):
|
||||
if "get-url" in argv:
|
||||
return "https://x-access-token:secret@github.com/org/Ext.git"
|
||||
raise AssertionError(argv)
|
||||
|
||||
def fake_run(argv, cwd=None):
|
||||
calls.append(argv)
|
||||
|
||||
monkeypatch.setattr(clone_ext, "out", fake_out)
|
||||
monkeypatch.setattr(clone_ext, "run", fake_run)
|
||||
clone_ext.scrub_origin(dest, "https://github.com/org/Ext.git")
|
||||
assert calls == [
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(dest),
|
||||
"remote",
|
||||
"set-url",
|
||||
"origin",
|
||||
"https://github.com/org/Ext.git",
|
||||
]
|
||||
]
|
||||
+45
-31
@@ -30,6 +30,32 @@ def _cfg(monkeypatch):
|
||||
return load_config(require_auth=True)
|
||||
|
||||
|
||||
def _mock_bind(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"gpu_rent.session.ensure_floating_ip",
|
||||
lambda conn, server, existing_id, existing_addr, log: ("203.0.113.9", "fip1"),
|
||||
)
|
||||
monkeypatch.setattr("gpu_rent.session.wait_ssh", lambda cfg, host, timeout=900.0: None)
|
||||
monkeypatch.setattr(
|
||||
"gpu_rent.session.run_ssh",
|
||||
lambda cfg, host, command, **kw: (
|
||||
"yes" if "gpu-rent-bootstrapped" in command else "inactive"
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gpu_rent.session.run_bootstrap",
|
||||
lambda cfg, host, log, update=True, light=False: None,
|
||||
)
|
||||
monkeypatch.setattr("gpu_rent.session.provision_vm", lambda cfg, host, log, **kw: None)
|
||||
monkeypatch.setattr("gpu_rent.session.wait_backend_idle", lambda cfg, host, log, **kw: None)
|
||||
monkeypatch.setattr(
|
||||
"gpu_rent.session.ensure_boot_snapshot",
|
||||
lambda conn, boot_volume_id, cfg, log: None,
|
||||
)
|
||||
monkeypatch.setattr("gpu_rent.session.notify_ready", lambda cfg, log: None)
|
||||
monkeypatch.setattr("gpu_rent.session.print_mcp_snippet", lambda cfg, log: None)
|
||||
|
||||
|
||||
def test_cmd_up_refuses_zero_gpu_quota(monkeypatch):
|
||||
monkeypatch.setattr("gpu_rent.session.connect", lambda cfg: object())
|
||||
monkeypatch.setattr("gpu_rent.session.compute_quotas", lambda conn: {"gpu": 0})
|
||||
@@ -42,24 +68,19 @@ def test_cmd_up_does_not_create_second_gpu(monkeypatch):
|
||||
monkeypatch.setattr("gpu_rent.session.connect", lambda cfg: object())
|
||||
monkeypatch.setattr("gpu_rent.session.compute_quotas", lambda conn: {"gpu": 1})
|
||||
monkeypatch.setattr("gpu_rent.session.pick_existing_server", lambda conn: Server())
|
||||
monkeypatch.setattr(
|
||||
"gpu_rent.session.ensure_floating_ip",
|
||||
lambda conn, server, existing_id, existing_addr, log: ("203.0.113.9", "fip1"),
|
||||
)
|
||||
monkeypatch.setattr("gpu_rent.session.wait_ssh", lambda cfg, host, timeout=420.0: None)
|
||||
monkeypatch.setattr("gpu_rent.session.run_bootstrap", lambda cfg, host, log: None)
|
||||
monkeypatch.setattr("gpu_rent.session.provision_vm", lambda cfg, host, log, **kw: None)
|
||||
monkeypatch.setattr("gpu_rent.session.wait_backend_idle", lambda cfg, host, log, **kw: None)
|
||||
monkeypatch.setattr(
|
||||
"gpu_rent.session.ensure_boot_snapshot",
|
||||
lambda conn, boot_volume_id, cfg, log: None,
|
||||
)
|
||||
monkeypatch.setattr("gpu_rent.session.notify_ready", lambda cfg, log: None)
|
||||
monkeypatch.setattr("gpu_rent.session.print_mcp_snippet", lambda cfg, log: None)
|
||||
_mock_bind(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"gpu_rent.session.create_gpu_server",
|
||||
lambda *a, **k: created.append("created") or Server(),
|
||||
)
|
||||
save_state(
|
||||
SessionState(
|
||||
server_id="s1",
|
||||
floating_ip="203.0.113.9",
|
||||
bootstrapped=True,
|
||||
phase="ready_cloud",
|
||||
)
|
||||
)
|
||||
state = cmd_up(_cfg(monkeypatch), yes=True)
|
||||
assert created == []
|
||||
assert state.server_id == "s1"
|
||||
@@ -80,20 +101,7 @@ def test_cmd_up_unshelves_expired(monkeypatch):
|
||||
"gpu_rent.session.unshelve",
|
||||
lambda conn, server, log: unshelved.append(server.id) or Server(status="ACTIVE"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gpu_rent.session.ensure_floating_ip",
|
||||
lambda conn, server, existing_id, existing_addr, log: ("203.0.113.9", "fip1"),
|
||||
)
|
||||
monkeypatch.setattr("gpu_rent.session.wait_ssh", lambda cfg, host, timeout=420.0: None)
|
||||
monkeypatch.setattr("gpu_rent.session.run_bootstrap", lambda cfg, host, log: None)
|
||||
monkeypatch.setattr("gpu_rent.session.provision_vm", lambda cfg, host, log, **kw: None)
|
||||
monkeypatch.setattr("gpu_rent.session.wait_backend_idle", lambda cfg, host, log, **kw: None)
|
||||
monkeypatch.setattr(
|
||||
"gpu_rent.session.ensure_boot_snapshot",
|
||||
lambda conn, boot_volume_id, cfg, log: None,
|
||||
)
|
||||
monkeypatch.setattr("gpu_rent.session.notify_ready", lambda cfg, log: None)
|
||||
monkeypatch.setattr("gpu_rent.session.print_mcp_snippet", lambda cfg, log: None)
|
||||
_mock_bind(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
"gpu_rent.session.create_gpu_server",
|
||||
lambda *a, **k: created.append("created"),
|
||||
@@ -108,8 +116,6 @@ def test_cmd_up_unshelves_expired(monkeypatch):
|
||||
|
||||
def test_cmd_stop_deletes_compute_keeps_disks(monkeypatch):
|
||||
deleted = []
|
||||
monkeypatch.setattr("gpu_rent.session.connect", lambda cfg: object())
|
||||
monkeypatch.setattr("gpu_rent.session.pick_existing_server", lambda conn: Server())
|
||||
monkeypatch.setattr(
|
||||
"gpu_rent.session.delete_server",
|
||||
lambda conn, server, log: deleted.append(server.id),
|
||||
@@ -119,7 +125,13 @@ def test_cmd_stop_deletes_compute_keeps_disks(monkeypatch):
|
||||
lambda conn, fip_id, address, log: deleted.append("fip"),
|
||||
)
|
||||
save_state(
|
||||
SessionState(server_id="s1", boot_volume_id="b1", data_volume_id="d1", floating_ip="1.1.1.1")
|
||||
SessionState(
|
||||
server_id="s1",
|
||||
boot_volume_id="b1",
|
||||
data_volume_id="d1",
|
||||
floating_ip="1.1.1.1",
|
||||
bootstrapped=True,
|
||||
)
|
||||
)
|
||||
|
||||
class Conn:
|
||||
@@ -129,10 +141,12 @@ def test_cmd_stop_deletes_compute_keeps_disks(monkeypatch):
|
||||
return Server(server_id=sid)
|
||||
|
||||
monkeypatch.setattr("gpu_rent.session.connect", lambda cfg: Conn())
|
||||
monkeypatch.setattr("gpu_rent.session.pick_existing_server", lambda conn: Server())
|
||||
state = cmd_stop(_cfg(monkeypatch))
|
||||
assert "s1" in deleted
|
||||
assert state.phase == "idle"
|
||||
assert state.server_id is None
|
||||
assert state.bootstrapped is False
|
||||
assert state.boot_volume_id == "b1"
|
||||
assert state.data_volume_id == "d1"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from gpu_rent.tunnel import decide_watch
|
||||
from gpu_rent.tunnel import decide_watch, tunnel_forwards
|
||||
|
||||
|
||||
def test_decide_ok_active():
|
||||
@@ -25,3 +25,25 @@ def test_decide_exit_error():
|
||||
def test_decide_exit_missing():
|
||||
d = decide_watch(None, tunnel_alive=False)
|
||||
assert d.kind == "exit"
|
||||
|
||||
|
||||
def test_tunnel_forwards_swarm_only(monkeypatch):
|
||||
class Cfg:
|
||||
swarmui_local_port = 17801
|
||||
llm_runtime = "none"
|
||||
ollama_local_port = 17811
|
||||
llamacpp_local_port = 17812
|
||||
|
||||
monkeypatch.setattr("gpu_rent.tunnel.load_state", lambda: type("S", (), {"notes": {}})())
|
||||
assert tunnel_forwards(Cfg()) == [(17801, 7801)]
|
||||
|
||||
|
||||
def test_tunnel_forwards_ollama(monkeypatch):
|
||||
class Cfg:
|
||||
swarmui_local_port = 17801
|
||||
llm_runtime = "ollama"
|
||||
ollama_local_port = 17811
|
||||
llamacpp_local_port = 17812
|
||||
|
||||
monkeypatch.setattr("gpu_rent.tunnel.load_state", lambda: type("S", (), {"notes": {}})())
|
||||
assert tunnel_forwards(Cfg()) == [(17801, 7801), (17811, 11434)]
|
||||
|
||||
Reference in New Issue
Block a user