gpu4agents 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gpu4agents/__init__.py +5 -0
- gpu4agents/__main__.py +4 -0
- gpu4agents/agent/__init__.py +1 -0
- gpu4agents/agent/install.py +173 -0
- gpu4agents/agent/jobs.py +193 -0
- gpu4agents/agent/mcp.py +646 -0
- gpu4agents/agent/skill/SKILL.md +68 -0
- gpu4agents/cli/__init__.py +0 -0
- gpu4agents/cli/_common.py +131 -0
- gpu4agents/cli/account.py +330 -0
- gpu4agents/cli/agent.py +74 -0
- gpu4agents/cli/inspect.py +207 -0
- gpu4agents/cli/lifecycle.py +127 -0
- gpu4agents/cli/main.py +163 -0
- gpu4agents/cli/providers.py +96 -0
- gpu4agents/cli/run.py +38 -0
- gpu4agents/cli/setup.py +164 -0
- gpu4agents/cli/status.py +224 -0
- gpu4agents/cli/timer.py +143 -0
- gpu4agents/cli/validate.py +57 -0
- gpu4agents/core/__init__.py +0 -0
- gpu4agents/core/beacon.py +133 -0
- gpu4agents/core/config.py +647 -0
- gpu4agents/core/credentials.py +95 -0
- gpu4agents/core/durations.py +47 -0
- gpu4agents/core/engine.py +1335 -0
- gpu4agents/core/errors.py +94 -0
- gpu4agents/core/events.py +117 -0
- gpu4agents/core/execution.py +169 -0
- gpu4agents/core/health.py +62 -0
- gpu4agents/core/hosts.py +58 -0
- gpu4agents/core/model_inspect.py +368 -0
- gpu4agents/core/models.py +313 -0
- gpu4agents/core/paths.py +42 -0
- gpu4agents/core/policy.py +264 -0
- gpu4agents/core/state.py +406 -0
- gpu4agents/core/sweep.py +478 -0
- gpu4agents/core/tool_parsers.py +63 -0
- gpu4agents/providers/__init__.py +4 -0
- gpu4agents/providers/base.py +137 -0
- gpu4agents/providers/registry.py +63 -0
- gpu4agents/providers/vast/__init__.py +3 -0
- gpu4agents/providers/vast/client.py +255 -0
- gpu4agents/providers/vast/config.py +56 -0
- gpu4agents/providers/vast/mapper.py +292 -0
- gpu4agents/providers/vast/provider.py +438 -0
- gpu4agents/runtime/__init__.py +6 -0
- gpu4agents/runtime/base.py +184 -0
- gpu4agents/runtime/vllm.py +295 -0
- gpu4agents/watchdog/__init__.py +3 -0
- gpu4agents/watchdog/agent_body.py +959 -0
- gpu4agents/watchdog/builder.py +248 -0
- gpu4agents-0.5.0.dist-info/METADATA +157 -0
- gpu4agents-0.5.0.dist-info/RECORD +59 -0
- gpu4agents-0.5.0.dist-info/WHEEL +4 -0
- gpu4agents-0.5.0.dist-info/entry_points.txt +2 -0
- gpu4agents-0.5.0.dist-info/licenses/LICENSE +21 -0
- jambu_gpu/__init__.py +9 -0
- jambu_gpu/__main__.py +4 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Client for the remote lifecycle watchdog.
|
|
2
|
+
|
|
3
|
+
The watchdog is the authority on activity: it holds the heartbeat state and
|
|
4
|
+
performs the shutdown. The CLI only reports activity to it and reads it back.
|
|
5
|
+
Every call is best-effort - losing the watchdog must never break a workload,
|
|
6
|
+
and an unreachable watchdog is surfaced as a warning, not a crash.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
from urllib.parse import quote
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from .errors import JambuError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WatchdogUnavailable(JambuError):
|
|
20
|
+
"""The remote watchdog could not be reached."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WatchdogClient:
|
|
24
|
+
def __init__(self, base_url: str, token: Optional[str], timeout: float = 10.0) -> None:
|
|
25
|
+
self.base_url = base_url.rstrip("/")
|
|
26
|
+
self.token = token
|
|
27
|
+
self.timeout = timeout
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def headers(self) -> dict[str, str]:
|
|
31
|
+
return {"X-Jambu-Token": self.token} if self.token else {}
|
|
32
|
+
|
|
33
|
+
# -- transport ----------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def _request(self, method: str, path: str, payload: Optional[dict] = None) -> dict[str, Any]:
|
|
36
|
+
url = f"{self.base_url}{path}"
|
|
37
|
+
try:
|
|
38
|
+
response = httpx.request(
|
|
39
|
+
method,
|
|
40
|
+
url,
|
|
41
|
+
json=payload,
|
|
42
|
+
headers=self.headers,
|
|
43
|
+
timeout=self.timeout,
|
|
44
|
+
)
|
|
45
|
+
except httpx.HTTPError as exc:
|
|
46
|
+
raise WatchdogUnavailable(f"watchdog unreachable at {url}: {exc}") from exc
|
|
47
|
+
if response.status_code == 401:
|
|
48
|
+
raise WatchdogUnavailable("watchdog rejected the control token")
|
|
49
|
+
if response.status_code >= 400:
|
|
50
|
+
raise WatchdogUnavailable(
|
|
51
|
+
f"watchdog returned {response.status_code}: {response.text[:200]}"
|
|
52
|
+
)
|
|
53
|
+
try:
|
|
54
|
+
return response.json()
|
|
55
|
+
except ValueError:
|
|
56
|
+
return {}
|
|
57
|
+
|
|
58
|
+
def try_request(self, method: str, path: str, payload: Optional[dict] = None) -> Optional[dict]:
|
|
59
|
+
try:
|
|
60
|
+
return self._request(method, path, payload)
|
|
61
|
+
except WatchdogUnavailable:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# -- routes -------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def ping(self) -> bool:
|
|
67
|
+
try:
|
|
68
|
+
self._request("GET", "/health")
|
|
69
|
+
return True
|
|
70
|
+
except WatchdogUnavailable:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def state(self) -> dict[str, Any]:
|
|
74
|
+
return self._request("GET", "/state")
|
|
75
|
+
|
|
76
|
+
def try_state(self) -> Optional[dict[str, Any]]:
|
|
77
|
+
return self.try_request("GET", "/state")
|
|
78
|
+
|
|
79
|
+
def events(self, limit: int = 100) -> list[dict[str, Any]]:
|
|
80
|
+
data = self.try_request("GET", f"/events?limit={limit}") or {}
|
|
81
|
+
return data.get("events", [])
|
|
82
|
+
|
|
83
|
+
def logs(self, name: str = "runtime", lines: int = 200) -> list[str]:
|
|
84
|
+
"""Tail of an instance log (runtime | onstart | watchdog).
|
|
85
|
+
|
|
86
|
+
Raises WatchdogUnavailable, so a caller can tell "empty" from "unreachable".
|
|
87
|
+
"""
|
|
88
|
+
data = self._request("GET", f"/logs?name={quote(name)}&lines={int(lines)}")
|
|
89
|
+
return [str(line) for line in data.get("lines") or []]
|
|
90
|
+
|
|
91
|
+
def identify(
|
|
92
|
+
self, instance_id: str, hourly_cost_usd: Optional[float] = None
|
|
93
|
+
) -> Optional[dict]:
|
|
94
|
+
return self.try_request(
|
|
95
|
+
"POST",
|
|
96
|
+
"/identify",
|
|
97
|
+
{"instance_id": instance_id, "hourly_cost_usd": hourly_cost_usd},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def activity(self) -> Optional[dict]:
|
|
101
|
+
return self.try_request("POST", "/activity")
|
|
102
|
+
|
|
103
|
+
def mark_starting(self) -> Optional[dict]:
|
|
104
|
+
return self.try_request("POST", "/starting")
|
|
105
|
+
|
|
106
|
+
def mark_ready(self) -> Optional[dict]:
|
|
107
|
+
return self.try_request("POST", "/ready")
|
|
108
|
+
|
|
109
|
+
def workload_start(self, workload_id: str, label: str = "") -> Optional[dict]:
|
|
110
|
+
return self.try_request(
|
|
111
|
+
"POST", "/workload/start", {"id": workload_id, "label": label}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def workload_heartbeat(self, workload_id: str) -> Optional[dict]:
|
|
115
|
+
return self.try_request("POST", "/workload/heartbeat", {"id": workload_id})
|
|
116
|
+
|
|
117
|
+
def workload_end(
|
|
118
|
+
self, workload_id: str, returncode: Optional[int], state: Optional[str] = None
|
|
119
|
+
) -> Optional[dict]:
|
|
120
|
+
return self.try_request(
|
|
121
|
+
"POST",
|
|
122
|
+
"/workload/end",
|
|
123
|
+
{"id": workload_id, "returncode": returncode, "state": state},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def lock(self, ttl_seconds: Optional[float], reason: str = "") -> dict:
|
|
127
|
+
return self._request("POST", "/lock", {"ttl_seconds": ttl_seconds, "reason": reason})
|
|
128
|
+
|
|
129
|
+
def unlock(self, reason: str = "manual") -> dict:
|
|
130
|
+
return self._request("POST", "/unlock", {"reason": reason})
|
|
131
|
+
|
|
132
|
+
def shutdown(self, action: str = "stop", reason: str = "manual") -> Optional[dict]:
|
|
133
|
+
return self.try_request("POST", "/shutdown", {"action": action, "reason": reason})
|