jambu-gpu 0.1.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.
Files changed (46) hide show
  1. jambu_gpu/__init__.py +5 -0
  2. jambu_gpu/__main__.py +4 -0
  3. jambu_gpu/cli/__init__.py +0 -0
  4. jambu_gpu/cli/_common.py +117 -0
  5. jambu_gpu/cli/inspect.py +207 -0
  6. jambu_gpu/cli/lifecycle.py +127 -0
  7. jambu_gpu/cli/main.py +103 -0
  8. jambu_gpu/cli/providers.py +96 -0
  9. jambu_gpu/cli/run.py +37 -0
  10. jambu_gpu/cli/setup.py +165 -0
  11. jambu_gpu/cli/status.py +214 -0
  12. jambu_gpu/cli/validate.py +57 -0
  13. jambu_gpu/core/__init__.py +0 -0
  14. jambu_gpu/core/beacon.py +124 -0
  15. jambu_gpu/core/config.py +596 -0
  16. jambu_gpu/core/credentials.py +75 -0
  17. jambu_gpu/core/durations.py +47 -0
  18. jambu_gpu/core/engine.py +951 -0
  19. jambu_gpu/core/errors.py +68 -0
  20. jambu_gpu/core/events.py +115 -0
  21. jambu_gpu/core/execution.py +169 -0
  22. jambu_gpu/core/health.py +62 -0
  23. jambu_gpu/core/model_inspect.py +368 -0
  24. jambu_gpu/core/models.py +283 -0
  25. jambu_gpu/core/policy.py +233 -0
  26. jambu_gpu/core/state.py +361 -0
  27. jambu_gpu/core/tool_parsers.py +63 -0
  28. jambu_gpu/providers/__init__.py +4 -0
  29. jambu_gpu/providers/base.py +125 -0
  30. jambu_gpu/providers/registry.py +63 -0
  31. jambu_gpu/providers/vast/__init__.py +3 -0
  32. jambu_gpu/providers/vast/client.py +230 -0
  33. jambu_gpu/providers/vast/config.py +51 -0
  34. jambu_gpu/providers/vast/mapper.py +221 -0
  35. jambu_gpu/providers/vast/provider.py +422 -0
  36. jambu_gpu/runtime/__init__.py +6 -0
  37. jambu_gpu/runtime/base.py +99 -0
  38. jambu_gpu/runtime/vllm.py +224 -0
  39. jambu_gpu/watchdog/__init__.py +3 -0
  40. jambu_gpu/watchdog/agent_body.py +547 -0
  41. jambu_gpu/watchdog/builder.py +178 -0
  42. jambu_gpu-0.1.0.dist-info/METADATA +82 -0
  43. jambu_gpu-0.1.0.dist-info/RECORD +46 -0
  44. jambu_gpu-0.1.0.dist-info/WHEEL +4 -0
  45. jambu_gpu-0.1.0.dist-info/entry_points.txt +2 -0
  46. jambu_gpu-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,233 @@
1
+ """Lifecycle policy (spec sections 10-13, 25).
2
+
3
+ This module is the SINGLE SOURCE OF TRUTH for "may this instance stay
4
+ billable?". It is deliberately stdlib-only and dependency-free because its
5
+ source is inlined verbatim into the remote watchdog agent that runs on the
6
+ rented instance (see jambu_gpu/watchdog/builder.py). Do not import anything
7
+ from the rest of the package here.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Optional
14
+
15
+ # --- normalized runtime state (spec section 8) -------------------------------
16
+
17
+ PROVISIONING = "provisioning"
18
+ STARTING = "starting"
19
+ READY = "ready"
20
+ BUSY = "busy"
21
+ IDLE = "idle"
22
+ STOPPED = "stopped"
23
+ FAILED = "failed"
24
+ DESTROYED = "destroyed"
25
+ UNKNOWN = "unknown"
26
+
27
+ # --- actions -----------------------------------------------------------------
28
+
29
+ ACTION_NONE = "none"
30
+ ACTION_STOP = "stop"
31
+ ACTION_DESTROY = "destroy"
32
+
33
+ # --- reasons (recorded on every automatic shutdown, spec criterion 12) --------
34
+
35
+ REASON_IDLE = "idle_timeout"
36
+ REASON_MAX_LIFETIME = "max_lifetime"
37
+ REASON_BUDGET = "budget_exceeded"
38
+
39
+
40
+ @dataclass
41
+ class Workload:
42
+ """A registered unit of useful work (spec section 12)."""
43
+
44
+ id: str
45
+ started_at: float
46
+ last_heartbeat: float
47
+ label: str = ""
48
+ state: str = "running" # running | queued | finished | failed
49
+
50
+ def is_pending(self) -> bool:
51
+ return self.state in ("running", "queued")
52
+
53
+ def is_stale(self, now: float, grace: float) -> bool:
54
+ """Heartbeat vanished for longer than the grace period (spec section 13)."""
55
+ if not self.is_pending():
56
+ return False
57
+ if self.state == "queued":
58
+ return False
59
+ return (now - self.last_heartbeat) > grace
60
+
61
+
62
+ @dataclass
63
+ class PolicyConfig:
64
+ idle_timeout_s: Optional[float] = None
65
+ max_lifetime_s: Optional[float] = None
66
+ heartbeat_grace_s: float = 300.0
67
+ auto_stop: bool = True
68
+ auto_destroy: bool = False
69
+ allow_indefinite_lock: bool = False
70
+ stop_supported: bool = True
71
+ destroy_supported: bool = True
72
+ # False when a stopped instance keeps costing real money (spec section 25).
73
+ stop_eliminates_billing: bool = True
74
+ # Cost guardrails (spec section 17).
75
+ hourly_cost_usd: Optional[float] = None
76
+ max_session_cost_usd: Optional[float] = None
77
+
78
+ @classmethod
79
+ def from_dict(cls, data: dict) -> "PolicyConfig":
80
+ known = {f for f in cls.__dataclass_fields__} # type: ignore[attr-defined]
81
+ return cls(**{k: v for k, v in data.items() if k in known})
82
+
83
+ def to_dict(self) -> dict:
84
+ return {f: getattr(self, f) for f in self.__dataclass_fields__} # type: ignore[attr-defined]
85
+
86
+
87
+ @dataclass
88
+ class Snapshot:
89
+ """Everything the policy needs to decide. Times are unix seconds."""
90
+
91
+ now: float
92
+ created_at: float
93
+ last_activity: float
94
+ workloads: list = field(default_factory=list) # list[Workload]
95
+ starting: bool = False
96
+ locked: bool = False
97
+ locked_until: Optional[float] = None
98
+ provider_state: str = READY
99
+
100
+ # -- derived helpers ----------------------------------------------------
101
+
102
+ def lock_active(self, cfg: PolicyConfig) -> bool:
103
+ if not self.locked:
104
+ return False
105
+ if self.locked_until is None:
106
+ # An indefinite lock only holds when the project explicitly allows it.
107
+ return bool(cfg.allow_indefinite_lock)
108
+ return self.locked_until > self.now
109
+
110
+ def lock_expired(self, cfg: PolicyConfig) -> bool:
111
+ if not self.locked:
112
+ return False
113
+ if self.locked_until is None:
114
+ return not cfg.allow_indefinite_lock
115
+ return self.locked_until <= self.now
116
+
117
+ def pending(self, cfg: PolicyConfig) -> list:
118
+ """Workloads that still count as useful work."""
119
+ return [
120
+ w
121
+ for w in self.workloads
122
+ if w.is_pending() and not w.is_stale(self.now, cfg.heartbeat_grace_s)
123
+ ]
124
+
125
+ def stale(self, cfg: PolicyConfig) -> list:
126
+ return [w for w in self.workloads if w.is_stale(self.now, cfg.heartbeat_grace_s)]
127
+
128
+ def idle_seconds(self, cfg: PolicyConfig) -> float:
129
+ """Seconds since the last thing that counted as activity."""
130
+ if self.pending(cfg) or self.starting:
131
+ return 0.0
132
+ marks = [self.last_activity]
133
+ for w in self.workloads:
134
+ marks.append(w.last_heartbeat)
135
+ return max(0.0, self.now - max(marks))
136
+
137
+ def age_seconds(self) -> float:
138
+ return max(0.0, self.now - self.created_at)
139
+
140
+
141
+ @dataclass
142
+ class Decision:
143
+ action: str = ACTION_NONE
144
+ reason: Optional[str] = None
145
+ detail: dict = field(default_factory=dict)
146
+
147
+ @property
148
+ def terminates(self) -> bool:
149
+ return self.action in (ACTION_STOP, ACTION_DESTROY)
150
+
151
+ def to_dict(self) -> dict:
152
+ return {"action": self.action, "reason": self.reason, "detail": dict(self.detail)}
153
+
154
+
155
+ def derive_state(snapshot: Snapshot, cfg: PolicyConfig) -> str:
156
+ """Normalized observed state of a live instance."""
157
+ if snapshot.provider_state in (STOPPED, DESTROYED, FAILED, PROVISIONING):
158
+ return snapshot.provider_state
159
+ if snapshot.starting:
160
+ return STARTING
161
+ if snapshot.pending(cfg):
162
+ return BUSY
163
+ if snapshot.provider_state == READY:
164
+ return IDLE if snapshot.idle_seconds(cfg) > 0 else READY
165
+ return snapshot.provider_state
166
+
167
+
168
+ def _terminal_action(cfg: PolicyConfig) -> str:
169
+ """Pick the cheapest supported action that actually stops the bleeding."""
170
+ if cfg.auto_destroy:
171
+ return ACTION_DESTROY if cfg.destroy_supported else ACTION_NONE
172
+ if not cfg.auto_stop:
173
+ return ACTION_NONE
174
+ if cfg.stop_supported and cfg.stop_eliminates_billing:
175
+ return ACTION_STOP
176
+ # Stopping does not (or cannot) end billing: escalate only when allowed to.
177
+ if not cfg.stop_supported:
178
+ return ACTION_DESTROY if cfg.destroy_supported else ACTION_NONE
179
+ return ACTION_STOP
180
+
181
+
182
+ def evaluate(snapshot: Snapshot, cfg: PolicyConfig) -> Decision:
183
+ """Core lifecycle rule (spec section 10). Pure function, no side effects."""
184
+ detail: dict[str, Any] = {
185
+ "state": derive_state(snapshot, cfg),
186
+ "age_seconds": round(snapshot.age_seconds(), 1),
187
+ "idle_seconds": round(snapshot.idle_seconds(cfg), 1),
188
+ "pending_workloads": len(snapshot.pending(cfg)),
189
+ "stale_workloads": len(snapshot.stale(cfg)),
190
+ "locked": snapshot.lock_active(cfg),
191
+ }
192
+
193
+ if snapshot.provider_state in (STOPPED, DESTROYED):
194
+ return Decision(ACTION_NONE, "already_terminal", detail)
195
+
196
+ locked = snapshot.lock_active(cfg)
197
+
198
+ # max_lifetime wins over everything except an active lock.
199
+ if cfg.max_lifetime_s is not None and snapshot.age_seconds() > cfg.max_lifetime_s:
200
+ if locked:
201
+ return Decision(ACTION_NONE, "locked", detail)
202
+ action = _terminal_action(cfg)
203
+ if action == ACTION_NONE:
204
+ return Decision(ACTION_NONE, "max_lifetime_no_action", detail)
205
+ return Decision(action, REASON_MAX_LIFETIME, detail)
206
+
207
+ # Session budget ceiling.
208
+ if cfg.max_session_cost_usd is not None and cfg.hourly_cost_usd:
209
+ spent = cfg.hourly_cost_usd * snapshot.age_seconds() / 3600.0
210
+ detail["session_cost_usd"] = round(spent, 4)
211
+ if spent > cfg.max_session_cost_usd:
212
+ if locked:
213
+ return Decision(ACTION_NONE, "locked", detail)
214
+ action = _terminal_action(cfg)
215
+ if action != ACTION_NONE:
216
+ return Decision(action, REASON_BUDGET, detail)
217
+
218
+ if locked:
219
+ return Decision(ACTION_NONE, "locked", detail)
220
+
221
+ # Useful work in flight is never interrupted (spec criterion 5).
222
+ if snapshot.starting:
223
+ return Decision(ACTION_NONE, "startup_in_progress", detail)
224
+ if snapshot.pending(cfg):
225
+ return Decision(ACTION_NONE, "workload_active", detail)
226
+
227
+ if cfg.idle_timeout_s is not None and snapshot.idle_seconds(cfg) > cfg.idle_timeout_s:
228
+ action = _terminal_action(cfg)
229
+ if action == ACTION_NONE:
230
+ return Decision(ACTION_NONE, "idle_no_action", detail)
231
+ return Decision(action, REASON_IDLE, detail)
232
+
233
+ return Decision(ACTION_NONE, "within_policy", detail)
@@ -0,0 +1,361 @@
1
+ """Observed runtime state (spec section 9).
2
+
3
+ jambu.yaml is the desired-state SoT; ``.jambu/state.json`` is what we last
4
+ observed. This module owns reading/writing it atomically, plus the on-disk
5
+ process lock that keeps two CLI invocations from provisioning twice.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import fcntl
11
+ import json
12
+ import os
13
+ import tempfile
14
+ import time
15
+ import uuid
16
+ from contextlib import contextmanager
17
+ from dataclasses import asdict, dataclass, field
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+ from typing import Any, Iterator, Optional
21
+
22
+ from .errors import StateError
23
+ from .models import Instance, InstanceState
24
+ from .policy import Workload as PolicyWorkload
25
+
26
+ STATE_VERSION = 1
27
+
28
+
29
+ def utc_now() -> float:
30
+ return time.time()
31
+
32
+
33
+ def to_iso(ts: Optional[float]) -> Optional[str]:
34
+ if ts is None:
35
+ return None
36
+ return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(timespec="seconds").replace(
37
+ "+00:00", "Z"
38
+ )
39
+
40
+
41
+ def from_iso(value: Any) -> Optional[float]:
42
+ if value is None or value == "":
43
+ return None
44
+ if isinstance(value, (int, float)):
45
+ return float(value)
46
+ text = str(value).replace("Z", "+00:00")
47
+ try:
48
+ return datetime.fromisoformat(text).timestamp()
49
+ except ValueError as exc:
50
+ raise StateError(f"invalid timestamp in state file: {value!r}") from exc
51
+
52
+
53
+ @dataclass
54
+ class WorkloadRecord:
55
+ id: str
56
+ label: str = ""
57
+ command: list[str] = field(default_factory=list)
58
+ state: str = "running" # running | queued | finished | failed
59
+ started_at: float = field(default_factory=utc_now)
60
+ last_heartbeat: float = field(default_factory=utc_now)
61
+ finished_at: Optional[float] = None
62
+ returncode: Optional[int] = None
63
+ pid: Optional[int] = None
64
+
65
+ def to_policy(self) -> PolicyWorkload:
66
+ return PolicyWorkload(
67
+ id=self.id,
68
+ started_at=self.started_at,
69
+ last_heartbeat=self.last_heartbeat,
70
+ label=self.label,
71
+ state=self.state,
72
+ )
73
+
74
+ def to_dict(self) -> dict[str, Any]:
75
+ data = asdict(self)
76
+ data["started_at"] = to_iso(self.started_at)
77
+ data["last_heartbeat"] = to_iso(self.last_heartbeat)
78
+ data["finished_at"] = to_iso(self.finished_at)
79
+ return data
80
+
81
+ @classmethod
82
+ def from_dict(cls, data: dict[str, Any]) -> "WorkloadRecord":
83
+ return cls(
84
+ id=data["id"],
85
+ label=data.get("label", ""),
86
+ command=list(data.get("command") or []),
87
+ state=data.get("state", "running"),
88
+ started_at=from_iso(data.get("started_at")) or utc_now(),
89
+ last_heartbeat=from_iso(data.get("last_heartbeat")) or utc_now(),
90
+ finished_at=from_iso(data.get("finished_at")),
91
+ returncode=data.get("returncode"),
92
+ pid=data.get("pid"),
93
+ )
94
+
95
+
96
+ @dataclass
97
+ class WatchdogInfo:
98
+ installed: bool = False
99
+ url: Optional[str] = None
100
+ token: Optional[str] = None
101
+ port: Optional[int] = None
102
+ last_contact: Optional[float] = None
103
+ last_error: Optional[str] = None
104
+
105
+ def to_dict(self) -> dict[str, Any]:
106
+ data = asdict(self)
107
+ data["last_contact"] = to_iso(self.last_contact)
108
+ return data
109
+
110
+ @classmethod
111
+ def from_dict(cls, data: dict[str, Any]) -> "WatchdogInfo":
112
+ return cls(
113
+ installed=bool(data.get("installed")),
114
+ url=data.get("url"),
115
+ token=data.get("token"),
116
+ port=data.get("port"),
117
+ last_contact=from_iso(data.get("last_contact")),
118
+ last_error=data.get("last_error"),
119
+ )
120
+
121
+
122
+ @dataclass
123
+ class RuntimeState:
124
+ provider: str = ""
125
+ instance_id: Optional[str] = None
126
+ fingerprint: Optional[str] = None
127
+ state: str = InstanceState.DESTROYED.value
128
+ created_at: Optional[float] = None
129
+ last_activity: Optional[float] = None
130
+ locked: bool = False
131
+ locked_until: Optional[float] = None
132
+ lock_reason: str = ""
133
+ starting: bool = False
134
+ model_id: Optional[str] = None
135
+ endpoint: Optional[str] = None
136
+ hourly_cost_usd: Optional[float] = None
137
+ session_cost_usd: float = 0.0
138
+ # Billable-time bookkeeping (spec section 25): session_cost_usd must
139
+ # reflect actual running time, not wall-clock time since creation - a
140
+ # stopped instance costs nothing in compute-hours even if `status` is
141
+ # checked hours later. `billed_seconds` accumulates completed billable
142
+ # periods (e.g. before a stop); `running_since` marks an open one.
143
+ billed_seconds: float = 0.0
144
+ running_since: Optional[float] = None
145
+ instance: Optional[Instance] = None
146
+ workloads: dict[str, WorkloadRecord] = field(default_factory=dict)
147
+ watchdog: WatchdogInfo = field(default_factory=WatchdogInfo)
148
+
149
+ # -- helpers ------------------------------------------------------------
150
+
151
+ def has_instance(self) -> bool:
152
+ return bool(self.instance_id) and self.state != InstanceState.DESTROYED.value
153
+
154
+ def touch(self) -> None:
155
+ self.last_activity = utc_now()
156
+
157
+ def accrued_billable_seconds(self, now: Optional[float] = None) -> float:
158
+ """Total seconds actually spent in a billable-compute state.
159
+
160
+ Unlike ``now - created_at``, this excludes time spent stopped -
161
+ checking `status` long after an instance auto-stopped must not keep
162
+ inflating the reported cost.
163
+ """
164
+ now = now if now is not None else utc_now()
165
+ open_period = max(0.0, now - self.running_since) if self.running_since is not None else 0.0
166
+ return self.billed_seconds + open_period
167
+
168
+ def mark_billable(self, is_billable: bool, now: Optional[float] = None) -> None:
169
+ """Record a transition into/out of a billable-compute state.
170
+
171
+ Call this every time reconciliation observes the current state -
172
+ it's a no-op unless billable-ness actually changed since the last call.
173
+ """
174
+ now = now if now is not None else utc_now()
175
+ was_billable = self.running_since is not None
176
+ if is_billable and not was_billable:
177
+ self.running_since = now
178
+ elif not is_billable and was_billable:
179
+ self.billed_seconds += max(0.0, now - self.running_since)
180
+ self.running_since = None
181
+
182
+ def pending_workloads(self) -> list[WorkloadRecord]:
183
+ return [w for w in self.workloads.values() if w.state in ("running", "queued")]
184
+
185
+ def prune_workloads(self, keep: int = 25) -> None:
186
+ finished = sorted(
187
+ (w for w in self.workloads.values() if w.state not in ("running", "queued")),
188
+ key=lambda w: w.finished_at or w.started_at,
189
+ )
190
+ for record in finished[:-keep] if len(finished) > keep else []:
191
+ self.workloads.pop(record.id, None)
192
+
193
+ def clear_instance(self, state: str = InstanceState.DESTROYED.value) -> None:
194
+ # Close out any open billable period and freeze the final total into
195
+ # session_cost_usd (the historical record of the finished session)
196
+ # before resetting the counters for whatever instance comes next.
197
+ self.mark_billable(False)
198
+ if self.hourly_cost_usd:
199
+ self.session_cost_usd = round(self.hourly_cost_usd * self.billed_seconds / 3600.0, 4)
200
+ self.instance_id = None
201
+ self.instance = None
202
+ self.endpoint = None
203
+ self.state = state
204
+ self.starting = False
205
+ self.locked = False
206
+ self.locked_until = None
207
+ self.watchdog = WatchdogInfo()
208
+ self.billed_seconds = 0.0
209
+ self.running_since = None
210
+ for record in self.workloads.values():
211
+ if record.state in ("running", "queued"):
212
+ record.state = "failed"
213
+ record.finished_at = utc_now()
214
+
215
+ # -- serialization ------------------------------------------------------
216
+
217
+ def to_dict(self) -> dict[str, Any]:
218
+ return {
219
+ "version": STATE_VERSION,
220
+ "provider": self.provider,
221
+ "instance_id": self.instance_id,
222
+ "fingerprint": self.fingerprint,
223
+ "state": self.state,
224
+ "created_at": to_iso(self.created_at),
225
+ "last_activity": to_iso(self.last_activity),
226
+ "locked": self.locked,
227
+ "locked_until": to_iso(self.locked_until),
228
+ "lock_reason": self.lock_reason,
229
+ "starting": self.starting,
230
+ "model_id": self.model_id,
231
+ "endpoint": self.endpoint,
232
+ "hourly_cost_usd": self.hourly_cost_usd,
233
+ "session_cost_usd": round(self.session_cost_usd, 4),
234
+ "billed_seconds": round(self.billed_seconds, 1),
235
+ "running_since": to_iso(self.running_since),
236
+ "instance": self.instance.to_dict() if self.instance else None,
237
+ "workloads": {k: v.to_dict() for k, v in self.workloads.items()},
238
+ "watchdog": self.watchdog.to_dict(),
239
+ }
240
+
241
+ @classmethod
242
+ def from_dict(cls, data: dict[str, Any]) -> "RuntimeState":
243
+ instance_data = data.get("instance")
244
+ return cls(
245
+ provider=data.get("provider", ""),
246
+ instance_id=data.get("instance_id"),
247
+ fingerprint=data.get("fingerprint"),
248
+ state=data.get("state", InstanceState.DESTROYED.value),
249
+ created_at=from_iso(data.get("created_at")),
250
+ last_activity=from_iso(data.get("last_activity")),
251
+ locked=bool(data.get("locked")),
252
+ locked_until=from_iso(data.get("locked_until")),
253
+ lock_reason=data.get("lock_reason", ""),
254
+ starting=bool(data.get("starting")),
255
+ model_id=data.get("model_id"),
256
+ endpoint=data.get("endpoint"),
257
+ hourly_cost_usd=data.get("hourly_cost_usd"),
258
+ session_cost_usd=float(data.get("session_cost_usd") or 0.0),
259
+ billed_seconds=float(data.get("billed_seconds") or 0.0),
260
+ running_since=from_iso(data.get("running_since")),
261
+ instance=Instance.from_dict(instance_data) if instance_data else None,
262
+ workloads={
263
+ k: WorkloadRecord.from_dict(v) for k, v in (data.get("workloads") or {}).items()
264
+ },
265
+ watchdog=WatchdogInfo.from_dict(data.get("watchdog") or {}),
266
+ )
267
+
268
+
269
+ class StateStore:
270
+ """Atomic reader/writer for ``.jambu/state.json``."""
271
+
272
+ def __init__(self, state_dir: Path) -> None:
273
+ self._depth = 0 # flock is per-open-file-description: keep it reentrant
274
+ self.dir = Path(state_dir)
275
+ self.path = self.dir / "state.json"
276
+ self.lock_path = self.dir / "runtime.lock"
277
+ self.logs_dir = self.dir / "logs"
278
+
279
+ def ensure_dirs(self) -> None:
280
+ self.dir.mkdir(parents=True, exist_ok=True)
281
+ self.logs_dir.mkdir(parents=True, exist_ok=True)
282
+ gitignore = self.dir / ".gitignore"
283
+ if not gitignore.exists():
284
+ gitignore.write_text("*\n")
285
+
286
+ def exists(self) -> bool:
287
+ return self.path.is_file()
288
+
289
+ def load(self) -> RuntimeState:
290
+ if not self.path.is_file():
291
+ return RuntimeState()
292
+ try:
293
+ data = json.loads(self.path.read_text() or "{}")
294
+ except json.JSONDecodeError as exc:
295
+ raise StateError(f"{self.path} is corrupt: {exc}") from exc
296
+ if not data:
297
+ return RuntimeState()
298
+ return RuntimeState.from_dict(data)
299
+
300
+ def save(self, state: RuntimeState) -> None:
301
+ self.ensure_dirs()
302
+ payload = json.dumps(state.to_dict(), indent=2, sort_keys=False) + "\n"
303
+ fd, tmp = tempfile.mkstemp(dir=str(self.dir), prefix=".state-", suffix=".json")
304
+ try:
305
+ with os.fdopen(fd, "w") as handle:
306
+ handle.write(payload)
307
+ handle.flush()
308
+ os.fsync(handle.fileno())
309
+ os.replace(tmp, self.path)
310
+ finally:
311
+ if os.path.exists(tmp):
312
+ os.unlink(tmp)
313
+
314
+ @contextmanager
315
+ def transaction(self, timeout: float = 30.0) -> Iterator[RuntimeState]:
316
+ """Exclusive read-modify-write. Guarantees `setup` cannot race itself."""
317
+ with self.process_lock(timeout=timeout):
318
+ state = self.load()
319
+ yield state
320
+ self.save(state)
321
+
322
+ @contextmanager
323
+ def process_lock(self, timeout: float = 30.0) -> Iterator[None]:
324
+ if self._depth > 0:
325
+ self._depth += 1
326
+ try:
327
+ yield
328
+ finally:
329
+ self._depth -= 1
330
+ return
331
+ self.ensure_dirs()
332
+ handle = open(self.lock_path, "a+")
333
+ deadline = time.time() + timeout
334
+ try:
335
+ while True:
336
+ try:
337
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
338
+ break
339
+ except BlockingIOError:
340
+ if time.time() >= deadline:
341
+ raise StateError(
342
+ "another gpu command is holding the runtime lock "
343
+ f"({self.lock_path}); retry when it finishes"
344
+ ) from None
345
+ time.sleep(0.2)
346
+ handle.seek(0)
347
+ handle.truncate()
348
+ handle.write(f"{os.getpid()} {to_iso(utc_now())}\n")
349
+ handle.flush()
350
+ self._depth = 1
351
+ yield
352
+ finally:
353
+ self._depth = 0
354
+ try:
355
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
356
+ finally:
357
+ handle.close()
358
+
359
+
360
+ def new_workload_id() -> str:
361
+ return uuid.uuid4().hex[:12]
@@ -0,0 +1,63 @@
1
+ """model.id -> vLLM ``--tool-call-parser`` heuristics.
2
+
3
+ Lives in ``core`` (not ``runtime``) because both the vLLM runtime and
4
+ ``core/model_inspect.py`` need it, and ``core`` must not depend on
5
+ ``runtime`` (Provider != Runtime != Core stays a one-way dependency).
6
+
7
+ Source: https://docs.vllm.ai/en/latest/features/tool_calling/ - keep this in
8
+ sync when vLLM adds parsers; override with runtime.tool_call_parser for
9
+ anything not recognized here (fine-tunes, quantized repos, etc.).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ _TOOL_PARSER_HINTS: tuple[tuple[str, str], ...] = (
15
+ ("llama-4", "llama4_pythonic"),
16
+ ("llama4", "llama4_pythonic"),
17
+ ("llama-3", "llama3_json"),
18
+ ("llama3", "llama3_json"),
19
+ ("hermes", "hermes"),
20
+ ("qwen2.5", "hermes"),
21
+ ("qwen-2.5", "hermes"),
22
+ ("qwen3-coder", "qwen3_xml"),
23
+ ("qwen3_coder", "qwen3_xml"),
24
+ # Qwen3.5/3.6/3.8 (hybrid Gated-DeltaNet architecture, vLLM >=0.17) emit
25
+ # tool calls in the SAME XML function/parameter format as Qwen3-Coder, not
26
+ # Hermes' JSON-in-XML format - confirmed empirically on
27
+ # empero-ai/Qwythos-9B-v2 (Qwen3.5-based): with --tool-call-parser hermes
28
+ # the model's <tool_call><function=x><parameter=y>...</tool_call> output
29
+ # landed as inert text in `content`; switching to qwen3_xml parses it into
30
+ # a real `tool_calls` array. Applies to this whole "point release" Qwen3.x
31
+ # family, unlike the original dash-versioned Qwen3 (e.g. Qwen3-8B), which
32
+ # does use hermes per Qwen's own docs.
33
+ ("qwen3.8", "qwen3_xml"),
34
+ ("qwen3_8", "qwen3_xml"),
35
+ ("qwen3.6", "qwen3_xml"),
36
+ ("qwen3_6", "qwen3_xml"),
37
+ ("qwen3.5", "qwen3_xml"),
38
+ ("qwen3_5", "qwen3_xml"),
39
+ ("qwen3", "hermes"), # original dash-versioned Qwen3 (e.g. Qwen3-8B): hermes
40
+ ("mistral", "mistral"),
41
+ ("mixtral", "mistral"),
42
+ ("granite-4", "granite4"),
43
+ ("granite4", "granite4"),
44
+ ("granite", "granite"),
45
+ ("internlm", "internlm"),
46
+ ("jamba", "jamba"),
47
+ ("xlam", "xlam"),
48
+ ("toolace", "pythonic"),
49
+ ("deepseek-v3.1", "deepseek_v31"),
50
+ ("deepseek-v3", "deepseek_v3"),
51
+ ("glm-4.5", "glm45"),
52
+ ("glm-4.7", "glm47"),
53
+ ("functiongemma", "functiongemma"),
54
+ )
55
+
56
+
57
+ def guess_tool_call_parser(model_id: str) -> "str | None":
58
+ """Best-effort ``--tool-call-parser`` guess from a HuggingFace repo id."""
59
+ lowered = model_id.lower()
60
+ for needle, parser in _TOOL_PARSER_HINTS:
61
+ if needle in lowered:
62
+ return parser
63
+ return None
@@ -0,0 +1,4 @@
1
+ from .base import GPUProvider, WatchdogSpec
2
+ from .registry import ProviderRegistry, registry
3
+
4
+ __all__ = ["GPUProvider", "WatchdogSpec", "ProviderRegistry", "registry"]