nvdc 0.2.3__tar.gz → 0.2.4__tar.gz
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.
- {nvdc-0.2.3 → nvdc-0.2.4}/PKG-INFO +2 -1
- {nvdc-0.2.3 → nvdc-0.2.4}/pyproject.toml +3 -1
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/__init__.py +1 -1
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/runtime.py +122 -5
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc.egg-info/PKG-INFO +2 -1
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc.egg-info/SOURCES.txt +1 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc.egg-info/requires.txt +1 -0
- nvdc-0.2.4/tests/test_ollama_autoinstall.py +83 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/README.md +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/setup.cfg +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/accounts.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/agent.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/app.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/attestation.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/catalog.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/cli.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/config.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/coordinator.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/gpu.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/hardware.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/house.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/inference.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/keys.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/models.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/payments.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/protocol.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/storage.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/store.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/update.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/wallet.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc/web/index.html +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc.egg-info/dependency_links.txt +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc.egg-info/entry_points.txt +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/src/nvdc.egg-info/top_level.txt +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_accounts_auth.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_coordinator_security.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_house_node.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_mlx_custody.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_model_attestation.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_persistence.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_self_serve.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_self_update.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_split_pricing.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_storage_money.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_stripe_webhook.py +0 -0
- {nvdc-0.2.3 → nvdc-0.2.4}/tests/test_version_gate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nvdc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Bring your GPU onto the network: one command turns a GPU into a verifiable, OpenAI-compatible inference node.
|
|
5
5
|
Author: NVDC
|
|
6
6
|
License: Apache-2.0
|
|
@@ -14,6 +14,7 @@ Requires-Dist: cryptography>=42.0
|
|
|
14
14
|
Requires-Dist: nvidia-ml-py>=12.535.77
|
|
15
15
|
Requires-Dist: redis>=5.0
|
|
16
16
|
Requires-Dist: stripe>=9.0
|
|
17
|
+
Requires-Dist: zstandard>=0.22
|
|
17
18
|
Provides-Extra: attestation
|
|
18
19
|
Requires-Dist: nv-attestation-sdk>=2.7.0; extra == "attestation"
|
|
19
20
|
Requires-Dist: nv-local-gpu-verifier>=2.7.0; extra == "attestation"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nvdc"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "Bring your GPU onto the network: one command turns a GPU into a verifiable, OpenAI-compatible inference node."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9"
|
|
@@ -15,6 +15,8 @@ dependencies = [
|
|
|
15
15
|
"nvidia-ml-py>=12.535.77",
|
|
16
16
|
"redis>=5.0",
|
|
17
17
|
"stripe>=9.0",
|
|
18
|
+
# Ollama distributes its runtime as .tar.zst; used by the auto-installer.
|
|
19
|
+
"zstandard>=0.22",
|
|
18
20
|
]
|
|
19
21
|
|
|
20
22
|
[project.optional-dependencies]
|
|
@@ -14,10 +14,14 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
16
|
import logging
|
|
17
|
+
import os
|
|
18
|
+
import platform
|
|
17
19
|
import shutil
|
|
18
20
|
import subprocess
|
|
21
|
+
import sys
|
|
19
22
|
import time
|
|
20
23
|
from dataclasses import asdict, dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
21
25
|
from typing import Optional
|
|
22
26
|
|
|
23
27
|
import httpx
|
|
@@ -36,6 +40,19 @@ log = logging.getLogger("nvdc.runtime")
|
|
|
36
40
|
|
|
37
41
|
OLLAMA_URL = "http://localhost:11434"
|
|
38
42
|
|
|
43
|
+
# User-space Ollama runtime, installed by the client itself when no system
|
|
44
|
+
# Ollama exists (no sudo required — the official tarball is self-contained).
|
|
45
|
+
OLLAMA_INSTALL_DIR = Path(os.path.expanduser("~/.nvdc/ollama"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _ollama_arch() -> str:
|
|
49
|
+
m = platform.machine().lower()
|
|
50
|
+
if m in ("x86_64", "amd64"):
|
|
51
|
+
return "amd64"
|
|
52
|
+
if m in ("aarch64", "arm64"):
|
|
53
|
+
return "arm64"
|
|
54
|
+
return ""
|
|
55
|
+
|
|
39
56
|
|
|
40
57
|
@dataclass
|
|
41
58
|
class LoadState:
|
|
@@ -259,8 +276,11 @@ class NodeRuntime:
|
|
|
259
276
|
return tip + "."
|
|
260
277
|
if any(k in m for k in ("not found", "no such model", "pull")):
|
|
261
278
|
return "Model couldn't be fetched — check your internet connection and try loading again."
|
|
262
|
-
if
|
|
263
|
-
return "
|
|
279
|
+
if "not installed" in m:
|
|
280
|
+
return ("On Linux the client installs Ollama automatically — retry the load. "
|
|
281
|
+
"On macOS: brew install --cask ollama (or use the MLX backend).")
|
|
282
|
+
if any(k in m for k in ("connection", "refused", "could not start")):
|
|
283
|
+
return "Ollama isn't running. Retry the load, or start it manually with 'ollama serve'."
|
|
264
284
|
return "Try a smaller model, or verify Ollama is installed and running, then reload."
|
|
265
285
|
|
|
266
286
|
async def _ollama_preflight(self, model_id: str):
|
|
@@ -290,6 +310,97 @@ class NodeRuntime:
|
|
|
290
310
|
self.load.detail = "warming model into memory (mock)"
|
|
291
311
|
await asyncio.sleep(0.6)
|
|
292
312
|
|
|
313
|
+
def _ollama_bin(self) -> Optional[str]:
|
|
314
|
+
"""The Ollama binary to use: system install, else our user-space one."""
|
|
315
|
+
found = shutil.which("ollama")
|
|
316
|
+
if found:
|
|
317
|
+
return found
|
|
318
|
+
local = OLLAMA_INSTALL_DIR / "bin" / "ollama"
|
|
319
|
+
return str(local) if local.exists() else None
|
|
320
|
+
|
|
321
|
+
async def _ollama_install(self):
|
|
322
|
+
"""Install the Ollama runtime user-space — the app owns its deps.
|
|
323
|
+
|
|
324
|
+
Uses the official self-contained tarball, extracted under ~/.nvdc —
|
|
325
|
+
no sudo, no system Python/packages touched. Linux only; macOS users
|
|
326
|
+
get it via the installer script (brew cask) or use the MLX backend.
|
|
327
|
+
"""
|
|
328
|
+
arch = _ollama_arch()
|
|
329
|
+
if sys.platform != "linux" or not arch:
|
|
330
|
+
hint = (" On macOS: brew install --cask ollama"
|
|
331
|
+
if sys.platform == "darwin" else "")
|
|
332
|
+
raise RuntimeError(
|
|
333
|
+
"Ollama is not installed. Install it from https://ollama.com" + hint)
|
|
334
|
+
self.load.status = "pulling"
|
|
335
|
+
self.load.progress = 0
|
|
336
|
+
self.load.detail = "installing the Ollama runtime (one-time setup)…"
|
|
337
|
+
OLLAMA_INSTALL_DIR.mkdir(parents=True, exist_ok=True)
|
|
338
|
+
# Ollama distributes .tar.zst today; keep .tgz as a legacy fallback.
|
|
339
|
+
tmp = None
|
|
340
|
+
last_err: Optional[Exception] = None
|
|
341
|
+
for suffix in (".tar.zst", ".tgz"):
|
|
342
|
+
url = f"https://ollama.com/download/ollama-linux-{arch}{suffix}"
|
|
343
|
+
candidate = OLLAMA_INSTALL_DIR / (".download" + suffix)
|
|
344
|
+
try:
|
|
345
|
+
log.info("installing the Ollama runtime from %s", url)
|
|
346
|
+
await self._download(url, candidate)
|
|
347
|
+
tmp = candidate
|
|
348
|
+
break
|
|
349
|
+
except Exception as e:
|
|
350
|
+
last_err = e
|
|
351
|
+
candidate.unlink(missing_ok=True)
|
|
352
|
+
if tmp is None:
|
|
353
|
+
raise RuntimeError(f"could not download the Ollama runtime: {last_err}")
|
|
354
|
+
self.load.detail = "unpacking the Ollama runtime…"
|
|
355
|
+
await asyncio.to_thread(self._extract_runtime, tmp)
|
|
356
|
+
tmp.unlink(missing_ok=True)
|
|
357
|
+
# Best-effort: expose `ollama` on the user's PATH for terminal use too.
|
|
358
|
+
try:
|
|
359
|
+
local_bin = Path(os.path.expanduser("~/.local/bin"))
|
|
360
|
+
local_bin.mkdir(parents=True, exist_ok=True)
|
|
361
|
+
link = local_bin / "ollama"
|
|
362
|
+
if not link.exists():
|
|
363
|
+
link.symlink_to(OLLAMA_INSTALL_DIR / "bin" / "ollama")
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
log.info("Ollama runtime installed to %s", OLLAMA_INSTALL_DIR)
|
|
367
|
+
|
|
368
|
+
async def _download(self, url: str, dest: Path):
|
|
369
|
+
async with httpx.AsyncClient(timeout=None, follow_redirects=True) as c:
|
|
370
|
+
async with c.stream("GET", url) as r:
|
|
371
|
+
r.raise_for_status()
|
|
372
|
+
total = int(r.headers.get("content-length") or 0)
|
|
373
|
+
done = 0
|
|
374
|
+
with open(dest, "wb") as f:
|
|
375
|
+
async for chunk in r.aiter_bytes(1024 * 1024):
|
|
376
|
+
f.write(chunk)
|
|
377
|
+
done += len(chunk)
|
|
378
|
+
if total:
|
|
379
|
+
self.load.progress = int(done * 100 / total)
|
|
380
|
+
self.load.detail = (
|
|
381
|
+
f"installing the Ollama runtime… "
|
|
382
|
+
f"{done // (1024*1024)} / {total // (1024*1024)} MB")
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def _extract_runtime(archive: Path):
|
|
386
|
+
import tarfile
|
|
387
|
+
|
|
388
|
+
def _extract(t: tarfile.TarFile):
|
|
389
|
+
try:
|
|
390
|
+
t.extractall(OLLAMA_INSTALL_DIR, filter="data")
|
|
391
|
+
except TypeError: # Python < 3.12 without the filter parameter
|
|
392
|
+
t.extractall(OLLAMA_INSTALL_DIR)
|
|
393
|
+
|
|
394
|
+
if archive.name.endswith(".tar.zst"):
|
|
395
|
+
import zstandard
|
|
396
|
+
with open(archive, "rb") as f:
|
|
397
|
+
with zstandard.ZstdDecompressor().stream_reader(f) as reader:
|
|
398
|
+
with tarfile.open(fileobj=reader, mode="r|") as t:
|
|
399
|
+
_extract(t)
|
|
400
|
+
else:
|
|
401
|
+
with tarfile.open(archive, "r:gz") as t:
|
|
402
|
+
_extract(t)
|
|
403
|
+
|
|
293
404
|
async def _ollama_ensure_server(self):
|
|
294
405
|
try:
|
|
295
406
|
async with httpx.AsyncClient(timeout=3) as c:
|
|
@@ -297,9 +408,15 @@ class NodeRuntime:
|
|
|
297
408
|
return
|
|
298
409
|
except Exception:
|
|
299
410
|
pass
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
411
|
+
binary = self._ollama_bin()
|
|
412
|
+
if binary is None:
|
|
413
|
+
# The app owns its dependencies: install the runtime, don't bail.
|
|
414
|
+
await self._ollama_install()
|
|
415
|
+
binary = self._ollama_bin()
|
|
416
|
+
if binary is None:
|
|
417
|
+
raise RuntimeError("Ollama install completed but the binary "
|
|
418
|
+
"was not found — please report this")
|
|
419
|
+
subprocess.Popen([binary, "serve"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
303
420
|
for _ in range(20):
|
|
304
421
|
await asyncio.sleep(0.5)
|
|
305
422
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nvdc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Bring your GPU onto the network: one command turns a GPU into a verifiable, OpenAI-compatible inference node.
|
|
5
5
|
Author: NVDC
|
|
6
6
|
License: Apache-2.0
|
|
@@ -14,6 +14,7 @@ Requires-Dist: cryptography>=42.0
|
|
|
14
14
|
Requires-Dist: nvidia-ml-py>=12.535.77
|
|
15
15
|
Requires-Dist: redis>=5.0
|
|
16
16
|
Requires-Dist: stripe>=9.0
|
|
17
|
+
Requires-Dist: zstandard>=0.22
|
|
17
18
|
Provides-Extra: attestation
|
|
18
19
|
Requires-Dist: nv-attestation-sdk>=2.7.0; extra == "attestation"
|
|
19
20
|
Requires-Dist: nv-local-gpu-verifier>=2.7.0; extra == "attestation"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""The client installs its own Ollama runtime instead of bailing out."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from nvdc import runtime as rt
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_arch_mapping(monkeypatch):
|
|
11
|
+
cases = {"x86_64": "amd64", "amd64": "amd64",
|
|
12
|
+
"aarch64": "arm64", "arm64": "arm64", "riscv64": ""}
|
|
13
|
+
for machine, expect in cases.items():
|
|
14
|
+
monkeypatch.setattr(rt.platform, "machine", lambda m=machine: m)
|
|
15
|
+
assert rt._ollama_arch() == expect
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture()
|
|
19
|
+
def runtime(monkeypatch, tmp_path):
|
|
20
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
21
|
+
return rt.NodeRuntime(name="t", mock=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_ollama_bin_prefers_system_then_local(runtime, monkeypatch, tmp_path):
|
|
25
|
+
monkeypatch.setattr(rt.shutil, "which", lambda _: "/usr/bin/ollama")
|
|
26
|
+
assert runtime._ollama_bin() == "/usr/bin/ollama"
|
|
27
|
+
|
|
28
|
+
monkeypatch.setattr(rt.shutil, "which", lambda _: None)
|
|
29
|
+
monkeypatch.setattr(rt, "OLLAMA_INSTALL_DIR", tmp_path / "ollama")
|
|
30
|
+
assert runtime._ollama_bin() is None
|
|
31
|
+
binpath = tmp_path / "ollama" / "bin"
|
|
32
|
+
binpath.mkdir(parents=True)
|
|
33
|
+
(binpath / "ollama").write_text("#!/bin/sh\n")
|
|
34
|
+
assert runtime._ollama_bin() == str(binpath / "ollama")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_ensure_server_installs_instead_of_bailing(runtime, monkeypatch):
|
|
38
|
+
"""The reported bug: 'Ollama is not installed' was a dead end."""
|
|
39
|
+
class _Boom(Exception):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
async def fake_install():
|
|
43
|
+
raise _Boom("install path reached")
|
|
44
|
+
|
|
45
|
+
monkeypatch.setattr(rt.shutil, "which", lambda _: None)
|
|
46
|
+
monkeypatch.setattr(rt, "OLLAMA_INSTALL_DIR", rt.Path("/nonexistent-nvdc-test"))
|
|
47
|
+
monkeypatch.setattr(runtime, "_ollama_install", fake_install)
|
|
48
|
+
with pytest.raises(_Boom):
|
|
49
|
+
asyncio.run(runtime._ollama_ensure_server())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_extract_runtime_handles_tar_zst(monkeypatch, tmp_path):
|
|
53
|
+
import io
|
|
54
|
+
import tarfile
|
|
55
|
+
import zstandard
|
|
56
|
+
|
|
57
|
+
# Build a tiny ollama-style .tar.zst (bin/ollama with exec bit).
|
|
58
|
+
buf = io.BytesIO()
|
|
59
|
+
with tarfile.open(fileobj=buf, mode="w") as t:
|
|
60
|
+
info = tarfile.TarInfo("bin/ollama")
|
|
61
|
+
payload = b"#!/bin/sh\necho ollama\n"
|
|
62
|
+
info.size = len(payload)
|
|
63
|
+
info.mode = 0o755
|
|
64
|
+
t.addfile(info, io.BytesIO(payload))
|
|
65
|
+
archive = tmp_path / ".download.tar.zst"
|
|
66
|
+
archive.write_bytes(zstandard.ZstdCompressor().compress(buf.getvalue()))
|
|
67
|
+
|
|
68
|
+
monkeypatch.setattr(rt, "OLLAMA_INSTALL_DIR", tmp_path / "ollama")
|
|
69
|
+
(tmp_path / "ollama").mkdir()
|
|
70
|
+
rt.NodeRuntime._extract_runtime(archive)
|
|
71
|
+
binary = tmp_path / "ollama" / "bin" / "ollama"
|
|
72
|
+
assert binary.exists()
|
|
73
|
+
assert binary.stat().st_mode & 0o111 # executable
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_install_refuses_unsupported_platform(runtime, monkeypatch):
|
|
77
|
+
monkeypatch.setattr(rt.sys, "platform", "darwin")
|
|
78
|
+
with pytest.raises(RuntimeError, match="brew install --cask ollama"):
|
|
79
|
+
asyncio.run(runtime._ollama_install())
|
|
80
|
+
monkeypatch.setattr(rt.sys, "platform", "linux")
|
|
81
|
+
monkeypatch.setattr(rt.platform, "machine", lambda: "riscv64")
|
|
82
|
+
with pytest.raises(RuntimeError, match="ollama.com"):
|
|
83
|
+
asyncio.run(runtime._ollama_install())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|