technode-cli 0.1.0__tar.gz → 0.2.0__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.
- {technode_cli-0.1.0/technode_cli.egg-info → technode_cli-0.2.0}/PKG-INFO +18 -2
- {technode_cli-0.1.0 → technode_cli-0.2.0}/README.md +16 -0
- {technode_cli-0.1.0 → technode_cli-0.2.0}/pyproject.toml +2 -2
- {technode_cli-0.1.0 → technode_cli-0.2.0}/technode/__init__.py +1 -1
- {technode_cli-0.1.0 → technode_cli-0.2.0}/technode/cli.py +4 -0
- technode_cli-0.2.0/technode/provider.py +248 -0
- {technode_cli-0.1.0 → technode_cli-0.2.0/technode_cli.egg-info}/PKG-INFO +18 -2
- {technode_cli-0.1.0 → technode_cli-0.2.0}/technode_cli.egg-info/SOURCES.txt +1 -0
- {technode_cli-0.1.0 → technode_cli-0.2.0}/LICENSE +0 -0
- {technode_cli-0.1.0 → technode_cli-0.2.0}/setup.cfg +0 -0
- {technode_cli-0.1.0 → technode_cli-0.2.0}/technode/__main__.py +0 -0
- {technode_cli-0.1.0 → technode_cli-0.2.0}/technode_cli.egg-info/dependency_links.txt +0 -0
- {technode_cli-0.1.0 → technode_cli-0.2.0}/technode_cli.egg-info/entry_points.txt +0 -0
- {technode_cli-0.1.0 → technode_cli-0.2.0}/technode_cli.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: technode-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: TechNode CLI — run inference on
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: TechNode CLI — run inference on compressed open models, rent GPUs, or serve as a provider (RunPod-compatible).
|
|
5
5
|
Author-email: TechNode <smlee3636@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://technode.network
|
|
@@ -35,6 +35,22 @@ Zero dependencies — pure Python stdlib, runs anywhere Python ≥3.8 does.
|
|
|
35
35
|
| `technode models [--json]` | List available models (id, quantization, role). |
|
|
36
36
|
| `technode infer PROMPT [-m MODEL] [-n MAX_TOKENS] [-t TEMP] [--json] [-q]` | Text generation. `-` or piped stdin reads the prompt from stdin. |
|
|
37
37
|
| `technode whoami` | Show the active key (masked) + endpoint. |
|
|
38
|
+
| `technode gpu lease/list/status/release` | Rent a whole GPU (Jupyter lab session). |
|
|
39
|
+
|
|
40
|
+
## Become a provider (share your GPU)
|
|
41
|
+
|
|
42
|
+
Got an NVIDIA Linux box? Join the grid and serve models — **outbound-only, works
|
|
43
|
+
behind any NAT** (no Tailscale, no inbound ports):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
technode provider register --gpu "RTX 4090" --vram 24
|
|
47
|
+
technode provider serve --llama-server /path/to/llama-server # pull-mode worker
|
|
48
|
+
technode provider status
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`serve` polls the broker for jobs it can run, executes them on your GPU, and
|
|
52
|
+
returns the results. Needs a llama.cpp `llama-server` binary (CUDA build for
|
|
53
|
+
NVIDIA) and operator approval before it receives live jobs.
|
|
38
54
|
|
|
39
55
|
## Configuration
|
|
40
56
|
|
|
@@ -21,6 +21,22 @@ Zero dependencies — pure Python stdlib, runs anywhere Python ≥3.8 does.
|
|
|
21
21
|
| `technode models [--json]` | List available models (id, quantization, role). |
|
|
22
22
|
| `technode infer PROMPT [-m MODEL] [-n MAX_TOKENS] [-t TEMP] [--json] [-q]` | Text generation. `-` or piped stdin reads the prompt from stdin. |
|
|
23
23
|
| `technode whoami` | Show the active key (masked) + endpoint. |
|
|
24
|
+
| `technode gpu lease/list/status/release` | Rent a whole GPU (Jupyter lab session). |
|
|
25
|
+
|
|
26
|
+
## Become a provider (share your GPU)
|
|
27
|
+
|
|
28
|
+
Got an NVIDIA Linux box? Join the grid and serve models — **outbound-only, works
|
|
29
|
+
behind any NAT** (no Tailscale, no inbound ports):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
technode provider register --gpu "RTX 4090" --vram 24
|
|
33
|
+
technode provider serve --llama-server /path/to/llama-server # pull-mode worker
|
|
34
|
+
technode provider status
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`serve` polls the broker for jobs it can run, executes them on your GPU, and
|
|
38
|
+
returns the results. Needs a llama.cpp `llama-server` binary (CUDA build for
|
|
39
|
+
NVIDIA) and operator approval before it receives live jobs.
|
|
24
40
|
|
|
25
41
|
## Configuration
|
|
26
42
|
|
|
@@ -6,8 +6,8 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
# Distribution name on PyPI. The console command is still `technode`
|
|
7
7
|
# (see [project.scripts]); PyPI disallows the bare name "technode".
|
|
8
8
|
name = "technode-cli"
|
|
9
|
-
version = "0.
|
|
10
|
-
description = "TechNode CLI — run inference on
|
|
9
|
+
version = "0.2.0"
|
|
10
|
+
description = "TechNode CLI — run inference on compressed open models, rent GPUs, or serve as a provider (RunPod-compatible)."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.8"
|
|
13
13
|
license = { text = "MIT" }
|
|
@@ -403,6 +403,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
403
403
|
|
|
404
404
|
gpu.set_defaults(func=lambda a: (gpu.print_help() or 0))
|
|
405
405
|
|
|
406
|
+
# provider: run a GPU as a serving node (separate concern from consuming)
|
|
407
|
+
from . import provider as _provider
|
|
408
|
+
_provider.add_parser(sub)
|
|
409
|
+
|
|
406
410
|
return p
|
|
407
411
|
|
|
408
412
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""TechNode provider commands — turn a Linux box into a grid serving node.
|
|
2
|
+
|
|
3
|
+
`technode provider serve` runs the stdlib-only serving daemon (pc_serve.py) in
|
|
4
|
+
*pull mode*: it polls the broker over outbound HTTPS for jobs it can serve, runs
|
|
5
|
+
them on the local GPU, and posts results back. No inbound port, no Tailscale —
|
|
6
|
+
works behind any NAT. This is the cross-platform / marketplace path.
|
|
7
|
+
|
|
8
|
+
technode provider register --gpu "RTX 4090" --vram 24
|
|
9
|
+
technode provider serve --llama-server /opt/llama.cpp/llama-server
|
|
10
|
+
technode provider status
|
|
11
|
+
|
|
12
|
+
Provider ops talk to the broker directly (long-poll doesn't fit a serverless
|
|
13
|
+
proxy's time limit). Override with TN_BROKER.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import urllib.error
|
|
22
|
+
import urllib.request
|
|
23
|
+
|
|
24
|
+
BROKER_URL = (os.environ.get("TN_BROKER", "").strip()
|
|
25
|
+
or "https://broker.technode.network").rstrip("/")
|
|
26
|
+
PC_SERVE_URL = "https://technode.network/agent/pc_serve.py"
|
|
27
|
+
TN_DIR = os.path.join(os.path.expanduser("~"), ".technode")
|
|
28
|
+
PROVIDER_CFG = os.path.join(TN_DIR, "provider.json")
|
|
29
|
+
PC_SERVE_PATH = os.path.join(TN_DIR, "pc_serve.py")
|
|
30
|
+
MODELS_DIR = os.path.join(TN_DIR, "models")
|
|
31
|
+
LLAMA_DIR = os.path.join(TN_DIR, "llama")
|
|
32
|
+
UA = "technode-cli-provider"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _die(msg, code=1):
|
|
36
|
+
print("technode: " + msg, file=sys.stderr)
|
|
37
|
+
raise SystemExit(code)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _req(method, url, body=None, timeout=30):
|
|
41
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
42
|
+
headers = {"User-Agent": UA, "Accept": "application/json"}
|
|
43
|
+
if data is not None:
|
|
44
|
+
headers["Content-Type"] = "application/json"
|
|
45
|
+
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
|
46
|
+
try:
|
|
47
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
48
|
+
return json.loads(r.read().decode() or "{}")
|
|
49
|
+
except urllib.error.HTTPError as e:
|
|
50
|
+
raw = e.read().decode("utf-8", "replace")
|
|
51
|
+
try:
|
|
52
|
+
payload = json.loads(raw)
|
|
53
|
+
except ValueError:
|
|
54
|
+
payload = {"error": raw[:300] or e.reason}
|
|
55
|
+
payload["_status"] = e.code
|
|
56
|
+
return payload
|
|
57
|
+
except urllib.error.URLError as e:
|
|
58
|
+
_die(f"cannot reach broker {BROKER_URL} — {e.reason}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_cfg():
|
|
62
|
+
try:
|
|
63
|
+
with open(PROVIDER_CFG, encoding="utf-8") as fh:
|
|
64
|
+
return json.load(fh)
|
|
65
|
+
except (FileNotFoundError, ValueError):
|
|
66
|
+
return {}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _save_cfg(cfg):
|
|
70
|
+
os.makedirs(TN_DIR, exist_ok=True)
|
|
71
|
+
tmp = PROVIDER_CFG + ".tmp"
|
|
72
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
73
|
+
json.dump(cfg, fh, indent=2)
|
|
74
|
+
os.replace(tmp, PROVIDER_CFG)
|
|
75
|
+
try:
|
|
76
|
+
os.chmod(PROVIDER_CFG, 0o600)
|
|
77
|
+
except OSError:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# --------------------------------------------------------------------------- #
|
|
82
|
+
def cmd_register(args):
|
|
83
|
+
import socket
|
|
84
|
+
body = {
|
|
85
|
+
"name": args.name or socket.gethostname(),
|
|
86
|
+
"hostname": socket.gethostname(),
|
|
87
|
+
"gpu_name": args.gpu or "",
|
|
88
|
+
"vram_gb": int(args.vram or 0),
|
|
89
|
+
"owner_email": args.email or "",
|
|
90
|
+
}
|
|
91
|
+
existing = _load_cfg()
|
|
92
|
+
if existing.get("register_token"):
|
|
93
|
+
# Re-register: prove ownership so the broker refreshes rather than rejects.
|
|
94
|
+
body["provider_id"] = existing.get("provider_id", "")
|
|
95
|
+
body["register_token"] = existing["register_token"]
|
|
96
|
+
res = _req("POST", BROKER_URL + "/provider/register", body)
|
|
97
|
+
if res.get("error"):
|
|
98
|
+
_die(f"register failed — {res.get('error')}")
|
|
99
|
+
cfg = {
|
|
100
|
+
"provider_id": res.get("provider_id"),
|
|
101
|
+
"register_token": res.get("register_token"),
|
|
102
|
+
"dashboard_token": res.get("dashboard_token"),
|
|
103
|
+
"broker": BROKER_URL,
|
|
104
|
+
}
|
|
105
|
+
_save_cfg(cfg)
|
|
106
|
+
print("Registered ✓")
|
|
107
|
+
print(f" provider_id: {cfg['provider_id']}")
|
|
108
|
+
print(f" dashboard: {res.get('dashboard_url', '')}")
|
|
109
|
+
print(f" creds saved: {PROVIDER_CFG} (chmod 600)")
|
|
110
|
+
print("\nNext: technode provider serve (needs operator approval to receive jobs)")
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cmd_status(args):
|
|
115
|
+
cfg = _load_cfg()
|
|
116
|
+
if not cfg.get("provider_id"):
|
|
117
|
+
print("Not registered. Run `technode provider register`.")
|
|
118
|
+
return 1
|
|
119
|
+
print(f"provider_id: {cfg['provider_id']}")
|
|
120
|
+
print(f"broker: {cfg.get('broker', BROKER_URL)}")
|
|
121
|
+
me = _req("GET", BROKER_URL + f"/provider/me?t={cfg.get('dashboard_token','')}", timeout=15)
|
|
122
|
+
if me.get("error"):
|
|
123
|
+
print(f"approval: unknown ({me.get('error')})")
|
|
124
|
+
else:
|
|
125
|
+
print(f"approved: {me.get('approved')}")
|
|
126
|
+
if me.get("serving_models"):
|
|
127
|
+
print(f"models: {', '.join(me['serving_models'])}")
|
|
128
|
+
llama = _find_llama(args.llama_server if hasattr(args, "llama_server") else None)
|
|
129
|
+
print(f"llama-server: {llama or 'NOT FOUND (set --llama-server or install llama.cpp)'}")
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _find_llama(explicit=None):
|
|
134
|
+
for cand in (explicit, os.environ.get("TN_LLAMA_BIN"),
|
|
135
|
+
shutil.which("llama-server"),
|
|
136
|
+
os.path.join(LLAMA_DIR, "llama-server"),
|
|
137
|
+
os.path.join(LLAMA_DIR, "build", "bin", "llama-server")):
|
|
138
|
+
if cand and os.path.isfile(cand) and os.access(cand, os.X_OK):
|
|
139
|
+
return cand
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _ensure_pc_serve():
|
|
144
|
+
if os.path.isfile(PC_SERVE_PATH):
|
|
145
|
+
return PC_SERVE_PATH
|
|
146
|
+
os.makedirs(TN_DIR, exist_ok=True)
|
|
147
|
+
print(f"downloading serving daemon → {PC_SERVE_PATH}")
|
|
148
|
+
req = urllib.request.Request(PC_SERVE_URL, headers={"User-Agent": UA})
|
|
149
|
+
try:
|
|
150
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
151
|
+
data = r.read()
|
|
152
|
+
except Exception as e:
|
|
153
|
+
_die(f"could not download pc_serve.py from {PC_SERVE_URL} — {e}")
|
|
154
|
+
with open(PC_SERVE_PATH, "wb") as fh:
|
|
155
|
+
fh.write(data)
|
|
156
|
+
return PC_SERVE_PATH
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def cmd_serve(args):
|
|
160
|
+
cfg = _load_cfg()
|
|
161
|
+
if not cfg.get("register_token"):
|
|
162
|
+
_die("not registered. Run `technode provider register` first.")
|
|
163
|
+
llama = _find_llama(args.llama_server)
|
|
164
|
+
if not llama:
|
|
165
|
+
_die("llama-server not found.\n"
|
|
166
|
+
" Point to it: technode provider serve --llama-server /path/to/llama-server\n"
|
|
167
|
+
" or set TN_LLAMA_BIN, or put it in ~/.technode/llama/.\n"
|
|
168
|
+
" NVIDIA build: https://github.com/ggml-org/llama.cpp/releases "
|
|
169
|
+
"(or build with -DGGML_CUDA=ON).")
|
|
170
|
+
pc_serve = _ensure_pc_serve()
|
|
171
|
+
os.makedirs(MODELS_DIR, exist_ok=True)
|
|
172
|
+
cmd = [sys.executable, pc_serve, "--pull",
|
|
173
|
+
"--provider-id", cfg["provider_id"],
|
|
174
|
+
"--register-token", cfg["register_token"],
|
|
175
|
+
"--bin", llama, "--models", MODELS_DIR]
|
|
176
|
+
if args.models:
|
|
177
|
+
cmd += ["--serve-models", args.models]
|
|
178
|
+
if args.no_inbound:
|
|
179
|
+
# pull mode doesn't need the inbound server; bind it to localhost only is
|
|
180
|
+
# not exposed via a flag, so we just note it. (Harmless when unreachable.)
|
|
181
|
+
pass
|
|
182
|
+
print(f"starting pull-mode serving: provider={cfg['provider_id']} llama={llama}")
|
|
183
|
+
print(f" broker={BROKER_URL} models-cache={MODELS_DIR}")
|
|
184
|
+
print(" (Ctrl-C to stop)\n")
|
|
185
|
+
env = dict(os.environ, TN_BROKER=BROKER_URL)
|
|
186
|
+
try:
|
|
187
|
+
return subprocess.call(cmd, env=env)
|
|
188
|
+
except KeyboardInterrupt:
|
|
189
|
+
return 130
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def cmd_install(args):
|
|
193
|
+
"""Emit a systemd unit that runs `technode provider serve` on boot."""
|
|
194
|
+
cfg = _load_cfg()
|
|
195
|
+
if not cfg.get("register_token"):
|
|
196
|
+
_die("register first: technode provider register")
|
|
197
|
+
tn = shutil.which("technode") or os.path.join(os.path.dirname(sys.executable), "technode")
|
|
198
|
+
llama = _find_llama(args.llama_server) or "/path/to/llama-server"
|
|
199
|
+
user = os.environ.get("USER", "root")
|
|
200
|
+
unit = f"""[Unit]
|
|
201
|
+
Description=TechNode provider (pull-mode serving)
|
|
202
|
+
After=network-online.target
|
|
203
|
+
Wants=network-online.target
|
|
204
|
+
|
|
205
|
+
[Service]
|
|
206
|
+
Type=simple
|
|
207
|
+
User={user}
|
|
208
|
+
ExecStart={tn} provider serve --llama-server {llama}
|
|
209
|
+
Restart=always
|
|
210
|
+
RestartSec=5
|
|
211
|
+
Environment=TN_BROKER={BROKER_URL}
|
|
212
|
+
|
|
213
|
+
[Install]
|
|
214
|
+
WantedBy=multi-user.target
|
|
215
|
+
"""
|
|
216
|
+
print("# Save as /etc/systemd/system/technode-provider.service, then:")
|
|
217
|
+
print("# sudo systemctl daemon-reload && sudo systemctl enable --now technode-provider")
|
|
218
|
+
print("# ----------------------------------------------------------------")
|
|
219
|
+
print(unit)
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def add_parser(sub):
|
|
224
|
+
p = sub.add_parser("provider", help="run a GPU as a grid serving node (Linux/cross-platform)")
|
|
225
|
+
psub = p.add_subparsers(dest="provider_command")
|
|
226
|
+
|
|
227
|
+
s = psub.add_parser("register", help="register this machine as a provider")
|
|
228
|
+
s.add_argument("--name", help="display name (default: hostname)")
|
|
229
|
+
s.add_argument("--gpu", help="GPU name, e.g. \"RTX 4090\"")
|
|
230
|
+
s.add_argument("--vram", help="GPU VRAM in GB")
|
|
231
|
+
s.add_argument("--email", help="owner email (optional)")
|
|
232
|
+
s.set_defaults(func=cmd_register)
|
|
233
|
+
|
|
234
|
+
s = psub.add_parser("serve", help="serve models in pull mode (outbound-only, NAT-friendly)")
|
|
235
|
+
s.add_argument("--llama-server", help="path to the llama-server binary")
|
|
236
|
+
s.add_argument("--models", help="comma-separated catalog ids to advertise (default: auto by VRAM)")
|
|
237
|
+
s.add_argument("--no-inbound", action="store_true", help="pull only (no inbound serving)")
|
|
238
|
+
s.set_defaults(func=cmd_serve)
|
|
239
|
+
|
|
240
|
+
s = psub.add_parser("status", help="show registration + approval + llama-server")
|
|
241
|
+
s.add_argument("--llama-server", help="path to the llama-server binary")
|
|
242
|
+
s.set_defaults(func=cmd_status)
|
|
243
|
+
|
|
244
|
+
s = psub.add_parser("install", help="print a systemd unit for boot persistence")
|
|
245
|
+
s.add_argument("--llama-server", help="path to the llama-server binary")
|
|
246
|
+
s.set_defaults(func=cmd_install)
|
|
247
|
+
|
|
248
|
+
p.set_defaults(func=lambda a: (p.print_help() or 0))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: technode-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: TechNode CLI — run inference on
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: TechNode CLI — run inference on compressed open models, rent GPUs, or serve as a provider (RunPod-compatible).
|
|
5
5
|
Author-email: TechNode <smlee3636@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://technode.network
|
|
@@ -35,6 +35,22 @@ Zero dependencies — pure Python stdlib, runs anywhere Python ≥3.8 does.
|
|
|
35
35
|
| `technode models [--json]` | List available models (id, quantization, role). |
|
|
36
36
|
| `technode infer PROMPT [-m MODEL] [-n MAX_TOKENS] [-t TEMP] [--json] [-q]` | Text generation. `-` or piped stdin reads the prompt from stdin. |
|
|
37
37
|
| `technode whoami` | Show the active key (masked) + endpoint. |
|
|
38
|
+
| `technode gpu lease/list/status/release` | Rent a whole GPU (Jupyter lab session). |
|
|
39
|
+
|
|
40
|
+
## Become a provider (share your GPU)
|
|
41
|
+
|
|
42
|
+
Got an NVIDIA Linux box? Join the grid and serve models — **outbound-only, works
|
|
43
|
+
behind any NAT** (no Tailscale, no inbound ports):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
technode provider register --gpu "RTX 4090" --vram 24
|
|
47
|
+
technode provider serve --llama-server /path/to/llama-server # pull-mode worker
|
|
48
|
+
technode provider status
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`serve` polls the broker for jobs it can run, executes them on your GPU, and
|
|
52
|
+
returns the results. Needs a llama.cpp `llama-server` binary (CUDA build for
|
|
53
|
+
NVIDIA) and operator approval before it receives live jobs.
|
|
38
54
|
|
|
39
55
|
## Configuration
|
|
40
56
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|