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:
Leonid Pershin
2026-08-21 05:29:23 +03:00
parent a9cf2e0f90
commit 2005b00175
43 changed files with 2258 additions and 197 deletions
+1 -1
View File
@@ -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):
+59
View File
@@ -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"
+87
View File
@@ -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"
+32
View File
@@ -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
View File
@@ -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"
+23 -1
View File
@@ -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)]