technode-cli 0.1.0__py3-none-any.whl → 0.2.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.
technode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """TechNode CLI — run inference on TechNode's compressed open-model GPU grid."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
technode/cli.py CHANGED
@@ -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
 
technode/provider.py ADDED
@@ -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.1.0
4
- Summary: TechNode CLI — run inference on TechNode's compressed open-model GPU grid (RunPod-compatible).
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
 
@@ -0,0 +1,10 @@
1
+ technode/__init__.py,sha256=t8NTvxRnioJRH5gE2COIcS3sHfgge-pZ4ObN0WPnETI,106
2
+ technode/__main__.py,sha256=hBG70kNbP-Vixov5nkJkcoh8Aws0E1a3MJjNDacxRP4,136
3
+ technode/cli.py,sha256=JzEqvFDAh5Lv0ewyyv7Po7eAqrTuJlZ87_mOl2SFETQ,15337
4
+ technode/provider.py,sha256=yvFbVTpQ6WERjwHl_xIlfGUd2ShtNlruaPeVJUb5YAo,9595
5
+ technode_cli-0.2.0.dist-info/licenses/LICENSE,sha256=dUZBEi7BA8K033XkmDt5oHuhCaG2QcmbZLBv5jPw9zI,1065
6
+ technode_cli-0.2.0.dist-info/METADATA,sha256=20Sy3-9N1mBoy6zh8K7zQPGU4OOkbg41SdSATrTwiU8,2676
7
+ technode_cli-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ technode_cli-0.2.0.dist-info/entry_points.txt,sha256=e2IUIHePhD2EYqlsMua7TWWQCmtTN_etV95FTZhQ0fo,47
9
+ technode_cli-0.2.0.dist-info/top_level.txt,sha256=PqqGyKCWgMCEx78tqZkiXmq4Y6qkkAm1KP7WA1NSfnM,9
10
+ technode_cli-0.2.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- technode/__init__.py,sha256=o5xSTehxhArSIzNV7FrRrir6SY_hl1nLnuU8RLW9veQ,106
2
- technode/__main__.py,sha256=hBG70kNbP-Vixov5nkJkcoh8Aws0E1a3MJjNDacxRP4,136
3
- technode/cli.py,sha256=34flilgXxBf_UT67HLuED-1NmYebQRyPAvlfhPVcOHU,15188
4
- technode_cli-0.1.0.dist-info/licenses/LICENSE,sha256=dUZBEi7BA8K033XkmDt5oHuhCaG2QcmbZLBv5jPw9zI,1065
5
- technode_cli-0.1.0.dist-info/METADATA,sha256=Tb6pVdlyVgZc1U2yawW_Net3mpos46gDxG_AEYjX1xI,2010
6
- technode_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
- technode_cli-0.1.0.dist-info/entry_points.txt,sha256=e2IUIHePhD2EYqlsMua7TWWQCmtTN_etV95FTZhQ0fo,47
8
- technode_cli-0.1.0.dist-info/top_level.txt,sha256=PqqGyKCWgMCEx78tqZkiXmq4Y6qkkAm1KP7WA1NSfnM,9
9
- technode_cli-0.1.0.dist-info/RECORD,,