tommytracx commited on
Commit
969297b
·
verified ·
1 Parent(s): e43376c

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. colab/THOX_Mesh_Node.ipynb +456 -0
colab/THOX_Mesh_Node.ipynb ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# THOX Mesh Node - Colab template\n",
8
+ "\n",
9
+ "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ttracx/thoxllm-cloud/blob/main/templates/colab/THOX_Mesh_Node.ipynb)\n",
10
+ "\n",
11
+ "Run a THOX model on a Colab VM and register it as **your own** node on\n",
12
+ "ThoxLLM Cloud.\n",
13
+ "\n",
14
+ "**You supply your own Hugging Face token. That is the only requirement.**\n",
15
+ "\n",
16
+ "Your token is entered into *this* Colab runtime with `getpass` - it is never\n",
17
+ "printed, never written to a file, and never sent to THOX. It is used only to\n",
18
+ "download model weights from Hugging Face.\n",
19
+ "\n",
20
+ "> **Opening this notebook.** The badge above resolves once `ttracx/thoxllm-cloud`\n",
21
+ "> is public. While it is private, download the notebook from the template Space\n",
22
+ "> (`https://huggingface.co/spaces/tommytracx/thox-mesh-node/blob/main/colab/THOX_Mesh_Node.ipynb`)\n",
23
+ "> and use **File -> Upload notebook** in Colab. Nothing else changes.\n",
24
+ "\n",
25
+ "---\n",
26
+ "\n",
27
+ "### What this notebook does\n",
28
+ "\n",
29
+ "1. Installs the serving runtime and the THOX registration helper.\n",
30
+ "2. Prompts for your HF token and your ThoxLLM Cloud API key.\n",
31
+ "3. Downloads a CPU-sized THOX model and serves it on `127.0.0.1:8000`\n",
32
+ " behind an OpenAI-compatible API.\n",
33
+ "4. Opens a Cloudflare tunnel, giving this VM a public URL.\n",
34
+ "5. Registers that URL so the gateway can route to it by alias.\n",
35
+ "6. Heartbeats so it keeps being routed to.\n",
36
+ "\n",
37
+ "Run the cells top to bottom."
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "markdown",
42
+ "metadata": {},
43
+ "source": [
44
+ "## 1. Install\n",
45
+ "\n",
46
+ "`thoxmesh_node` is the thin registration helper. It is published as a wheel\n",
47
+ "alongside the template Space on Hugging Face, so this cell needs no GitHub\n",
48
+ "access and no credentials."
49
+ ]
50
+ },
51
+ {
52
+ "cell_type": "code",
53
+ "execution_count": null,
54
+ "metadata": {},
55
+ "outputs": [],
56
+ "source": [
57
+ "%pip install -q llama-cpp-python huggingface_hub\n",
58
+ "%pip install -q \"thoxmesh-node @ https://huggingface.co/tommytracx/thoxmesh-node-dist/resolve/main/dist/thoxmesh_node-0.1.0-py3-none-any.whl\"\n",
59
+ "\n",
60
+ "import thoxmesh_node\n",
61
+ "print('thoxmesh_node', thoxmesh_node.__version__)"
62
+ ]
63
+ },
64
+ {
65
+ "cell_type": "markdown",
66
+ "metadata": {},
67
+ "source": [
68
+ "## 2. Credentials\n",
69
+ "\n",
70
+ "`getpass` keeps these out of the notebook output and out of your saved copy.\n",
71
+ "\n",
72
+ "- **HF token** - https://huggingface.co/settings/tokens (`read` scope is enough).\n",
73
+ "- **ThoxLLM Cloud URL + API key** - from the portal, *Deploy your own mesh*.\n",
74
+ " The key needs the `mesh:register` scope. **It is not your HF token**, and\n",
75
+ " registration rejects `hf_*` values.\n",
76
+ "\n",
77
+ "Leave the cloud fields blank to serve the model without registering it."
78
+ ]
79
+ },
80
+ {
81
+ "cell_type": "code",
82
+ "execution_count": null,
83
+ "metadata": {},
84
+ "outputs": [],
85
+ "source": [
86
+ "import os\n",
87
+ "from getpass import getpass\n",
88
+ "\n",
89
+ "os.environ['HF_TOKEN'] = getpass('Your Hugging Face token: ').strip()\n",
90
+ "os.environ['THOX_CLOUD_URL'] = input('ThoxLLM Cloud URL (blank to skip): ').strip()\n",
91
+ "if os.environ['THOX_CLOUD_URL']:\n",
92
+ " os.environ['THOX_API_KEY'] = getpass('ThoxLLM Cloud API key (mesh:register): ').strip()\n",
93
+ "\n",
94
+ "# Sanity checks only - never print either credential.\n",
95
+ "assert os.environ['HF_TOKEN'].startswith('hf_'), 'That does not look like an HF token'\n",
96
+ "if os.environ.get('THOX_API_KEY', '').startswith('hf_'):\n",
97
+ " raise SystemExit('That is your Hugging Face token, not a ThoxLLM Cloud API '\n",
98
+ " 'key. Your HF token never leaves this runtime.')\n",
99
+ "print('HF token accepted (%d chars)' % len(os.environ['HF_TOKEN']))\n",
100
+ "print('Cloud:', os.environ['THOX_CLOUD_URL'] or '(not registering)')"
101
+ ]
102
+ },
103
+ {
104
+ "cell_type": "markdown",
105
+ "metadata": {},
106
+ "source": [
107
+ "## 3. Choose a model\n",
108
+ "\n",
109
+ "The default is the smallest model that still produces coherent output on a\n",
110
+ "free CPU runtime. Swap in a larger THOX model if you have a GPU runtime.\n",
111
+ "\n",
112
+ "| Model | File | Size | Notes |\n",
113
+ "|---|---|---|---|\n",
114
+ "| `Thox-ai/ThoxMini-3B` | `thoxmini-3b-Q4_K_M.gguf` | 2.0 GB | **default** - coherent output |\n",
115
+ "| `Thox-ai/thox-micro-125m-GGUF` | `thox-micro-125m.q4_k_m.gguf` | 85 MB | loads in seconds, but output is degenerate - plumbing tests only |"
116
+ ]
117
+ },
118
+ {
119
+ "cell_type": "code",
120
+ "execution_count": null,
121
+ "metadata": {},
122
+ "outputs": [],
123
+ "source": [
124
+ "MODEL_REPO = 'Thox-ai/ThoxMini-3B' # @param {type:'string'}\n",
125
+ "MODEL_FILE = 'thoxmini-3b-Q4_K_M.gguf' # @param {type:'string'}\n",
126
+ "ALIAS = 'thoxmini-3b' # @param {type:'string'}\n",
127
+ "PORT = 8000\n",
128
+ "CONTEXT = 2048\n",
129
+ "\n",
130
+ "os.environ['THOX_MODEL_ID'] = MODEL_REPO\n",
131
+ "os.environ['THOX_ALIAS'] = ALIAS\n",
132
+ "\n",
133
+ "from huggingface_hub import hf_hub_download\n",
134
+ "\n",
135
+ "model_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)\n",
136
+ "print('Downloaded to', model_path)"
137
+ ]
138
+ },
139
+ {
140
+ "cell_type": "markdown",
141
+ "metadata": {},
142
+ "source": [
143
+ "## 4. Serve the model\n",
144
+ "\n",
145
+ "`llama_cpp.server` exposes an OpenAI-compatible API. It binds to loopback;\n",
146
+ "the next cell puts a tunnel in front of it so the gateway can reach it."
147
+ ]
148
+ },
149
+ {
150
+ "cell_type": "code",
151
+ "execution_count": null,
152
+ "metadata": {},
153
+ "outputs": [],
154
+ "source": [
155
+ "import subprocess, sys, time, urllib.request\n",
156
+ "\n",
157
+ "server = subprocess.Popen(\n",
158
+ " [sys.executable, '-m', 'llama_cpp.server',\n",
159
+ " '--model', model_path,\n",
160
+ " '--host', '127.0.0.1',\n",
161
+ " '--port', str(PORT),\n",
162
+ " '--n_ctx', str(CONTEXT),\n",
163
+ " '--model_alias', ALIAS],\n",
164
+ " stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n",
165
+ ")\n",
166
+ "\n",
167
+ "def wait_for_server(url, timeout=300):\n",
168
+ " deadline = time.time() + timeout\n",
169
+ " while time.time() < deadline:\n",
170
+ " if server.poll() is not None:\n",
171
+ " raise RuntimeError('Model server exited:\\n' + (server.stdout.read() or ''))\n",
172
+ " try:\n",
173
+ " with urllib.request.urlopen(url, timeout=5) as response:\n",
174
+ " if response.status == 200:\n",
175
+ " return True\n",
176
+ " except Exception:\n",
177
+ " time.sleep(2)\n",
178
+ " raise TimeoutError('Model server did not become ready in %ss' % timeout)\n",
179
+ "\n",
180
+ "wait_for_server(f'http://127.0.0.1:{PORT}/v1/models')\n",
181
+ "print('Model server ready on 127.0.0.1:%d' % PORT)"
182
+ ]
183
+ },
184
+ {
185
+ "cell_type": "markdown",
186
+ "metadata": {},
187
+ "source": [
188
+ "### Quick check"
189
+ ]
190
+ },
191
+ {
192
+ "cell_type": "code",
193
+ "execution_count": null,
194
+ "metadata": {},
195
+ "outputs": [],
196
+ "source": [
197
+ "import json, urllib.request\n",
198
+ "\n",
199
+ "payload = json.dumps({\n",
200
+ " 'model': ALIAS,\n",
201
+ " 'messages': [{'role': 'user', 'content': 'Say hello in five words.'}],\n",
202
+ " 'max_tokens': 32,\n",
203
+ "}).encode()\n",
204
+ "request = urllib.request.Request(\n",
205
+ " f'http://127.0.0.1:{PORT}/v1/chat/completions',\n",
206
+ " data=payload, headers={'content-type': 'application/json'})\n",
207
+ "with urllib.request.urlopen(request, timeout=120) as response:\n",
208
+ " reply = json.load(response)\n",
209
+ "print(reply['choices'][0]['message']['content'])"
210
+ ]
211
+ },
212
+ {
213
+ "cell_type": "markdown",
214
+ "metadata": {},
215
+ "source": [
216
+ "## 5. Tunnel - how the gateway reaches this node\n",
217
+ "\n",
218
+ "A Colab VM has no inbound address, so it needs a public URL before the\n",
219
+ "ThoxLLM Cloud gateway can proxy to it. `cloudflared` provides one.\n",
220
+ "\n",
221
+ "> **This URL is unauthenticated** - anyone holding it can call your model.\n",
222
+ "> Set `THOX_UPSTREAM_AUTH=bearer` with a credential you mint if that matters.\n",
223
+ "> It is also ephemeral: a new tunnel means re-registering.\n",
224
+ "\n",
225
+ "Skip only if you are registering a private address into your own mesh with\n",
226
+ "`THOX_REGISTRAR=device_v2`."
227
+ ]
228
+ },
229
+ {
230
+ "cell_type": "code",
231
+ "execution_count": null,
232
+ "metadata": {},
233
+ "outputs": [],
234
+ "source": [
235
+ "RUN_TUNNEL = True # @param {type:'boolean'}\n",
236
+ "\n",
237
+ "public_url = None\n",
238
+ "if RUN_TUNNEL:\n",
239
+ " !wget -q -O cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64\n",
240
+ " !chmod +x cloudflared\n",
241
+ " import re, threading\n",
242
+ " tunnel = subprocess.Popen(\n",
243
+ " ['./cloudflared', 'tunnel', '--no-autoupdate', '--url', f'http://127.0.0.1:{PORT}'],\n",
244
+ " stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n",
245
+ "\n",
246
+ " def read_tunnel_url():\n",
247
+ " global public_url\n",
248
+ " for line in tunnel.stdout:\n",
249
+ " match = re.search(r'https://[-a-z0-9]+\\.trycloudflare\\.com', line)\n",
250
+ " if match:\n",
251
+ " public_url = match.group(0)\n",
252
+ " print('Tunnel (yours, not the mesh address):', public_url)\n",
253
+ " return\n",
254
+ "\n",
255
+ " threading.Thread(target=read_tunnel_url, daemon=True).start()\n",
256
+ " time.sleep(15)\n",
257
+ "\n",
258
+ "os.environ['THOX_PUBLIC_URL'] = public_url or ''\n",
259
+ "print('Public URL:', public_url or '(none)')"
260
+ ]
261
+ },
262
+ {
263
+ "cell_type": "markdown",
264
+ "metadata": {},
265
+ "source": [
266
+ "## 6. Register your node\n",
267
+ "\n",
268
+ "Two paths, wanting **opposite** addresses:\n",
269
+ "\n",
270
+ "| Registrar | Advertises | Use when |\n",
271
+ "|---|---|---|\n",
272
+ "| `spine` *(default)* | the public tunnel URL | Normal BYO. The gateway proxies to you. |\n",
273
+ "| `device_v2` | a private address | Node-to-node routing in your own mesh. |\n",
274
+ "\n",
275
+ "| Attach mode | Advertises | Valid for |\n",
276
+ "|---|---|---|\n",
277
+ "| `public` | the tunnel URL | `spine` |\n",
278
+ "| `mesh` | assigned `100.96.x.x` | `device_v2`, WireGuard up |\n",
279
+ "| `lan` / `loopback` | private address | `device_v2` |"
280
+ ]
281
+ },
282
+ {
283
+ "cell_type": "code",
284
+ "execution_count": null,
285
+ "metadata": {},
286
+ "outputs": [],
287
+ "source": [
288
+ "from thoxmesh_node import AttachError, NodeAgent, NodeConfig\n",
289
+ "\n",
290
+ "REGISTRAR = 'spine' # @param ['spine', 'device_v2']\n",
291
+ "ATTACH_MODE = 'public' # @param ['public', 'loopback', 'mesh', 'lan']\n",
292
+ "\n",
293
+ "agent = None\n",
294
+ "if os.environ.get('THOX_CLOUD_URL') and os.environ.get('THOX_API_KEY'):\n",
295
+ " config = NodeConfig.from_env(\n",
296
+ " alias=ALIAS,\n",
297
+ " port=PORT,\n",
298
+ " context_window=CONTEXT,\n",
299
+ " registrar=REGISTRAR,\n",
300
+ " attach_mode=ATTACH_MODE,\n",
301
+ " display_name='THOX Colab Node',\n",
302
+ " product_type='other',\n",
303
+ " platform='linux',\n",
304
+ " role='compute',\n",
305
+ " capabilities={'chat': True, 'completions': True},\n",
306
+ " state_dir=__import__('pathlib').Path('/content/.thoxmesh'),\n",
307
+ " app_version='colab',\n",
308
+ " )\n",
309
+ " agent = NodeAgent(config)\n",
310
+ " try:\n",
311
+ " endpoint_id = agent.join()\n",
312
+ " print('Registered via', REGISTRAR)\n",
313
+ " print(' endpoint', endpoint_id)\n",
314
+ " print(' address ', agent.attachment.base_url)\n",
315
+ " if agent.device is not None:\n",
316
+ " print(' device ', agent.device.device_id)\n",
317
+ " except AttachError as error:\n",
318
+ " print('Could not advertise an address:\\n', error)\n",
319
+ "else:\n",
320
+ " print('Not registering - serving locally only.')"
321
+ ]
322
+ },
323
+ {
324
+ "cell_type": "markdown",
325
+ "metadata": {},
326
+ "source": [
327
+ "## 7. Verify\n",
328
+ "\n",
329
+ "On the `device_v2` path this asks the control plane to resolve your model\n",
330
+ "and confirms it selects this node - the same path `ThoxRoute` uses. On the\n",
331
+ "`spine` path it prints how to call your alias through the gateway."
332
+ ]
333
+ },
334
+ {
335
+ "cell_type": "code",
336
+ "execution_count": null,
337
+ "metadata": {},
338
+ "outputs": [],
339
+ "source": [
340
+ "if agent is not None and agent.endpoint_id and agent.client and agent.device:\n",
341
+ " peers = agent.client.list_peers(agent.device.device_id)\n",
342
+ " print('Peers in mesh:', len(peers['peers']))\n",
343
+ " for peer in peers['peers']:\n",
344
+ " marker = ' <- this node' if peer['id'] == agent.device.device_id else ''\n",
345
+ " print(' %-38s %-8s %s%s' % (peer['id'], peer['status'], peer.get('mesh_ip'), marker))\n",
346
+ "\n",
347
+ " route = agent.client.device_action(\n",
348
+ " agent.device.device_id, 'resolve_route', {'model_id': os.environ['THOX_MODEL_ID']})\n",
349
+ " endpoint = route['route']['endpoint']\n",
350
+ " print('\\nresolve_route selected:', endpoint['id'], 'at', endpoint['base_url'])\n",
351
+ " assert endpoint['id'] == agent.endpoint_id, 'Mesh routed to a different node'\n",
352
+ " print('OK - this node is registered and routable.')\n",
353
+ "elif agent is not None and agent.endpoint_id:\n",
354
+ " print('Registered with the gateway. Address it by alias:')\n",
355
+ " print(' POST %s/v1/chat/completions' % os.environ['THOX_CLOUD_URL'])\n",
356
+ " print(' authorization: Bearer <your API key>')\n",
357
+ " print(' body: {\"model\": \"%s\", \"messages\": [...]}' % ALIAS)"
358
+ ]
359
+ },
360
+ {
361
+ "cell_type": "markdown",
362
+ "metadata": {},
363
+ "source": [
364
+ "## 8. Keep the node alive\n",
365
+ "\n",
366
+ "The agent heartbeats on a background thread. Leave this cell running to keep\n",
367
+ "the Colab session (and the node) up. Interrupt it to stop.\n",
368
+ "\n",
369
+ "When the session ends the node stops heartbeating and the mesh stops\n",
370
+ "routing to it."
371
+ ]
372
+ },
373
+ {
374
+ "cell_type": "code",
375
+ "execution_count": null,
376
+ "metadata": {},
377
+ "outputs": [],
378
+ "source": [
379
+ "import time\n",
380
+ "\n",
381
+ "try:\n",
382
+ " while True:\n",
383
+ " time.sleep(60)\n",
384
+ " if agent is not None:\n",
385
+ " print(time.strftime('%H:%M:%S'), 'heartbeat ok')\n",
386
+ "except KeyboardInterrupt:\n",
387
+ " print('Stopping...')\n",
388
+ "finally:\n",
389
+ " if agent is not None:\n",
390
+ " agent.stop()\n",
391
+ " print('Node unregistered.')"
392
+ ]
393
+ },
394
+ {
395
+ "cell_type": "markdown",
396
+ "metadata": {},
397
+ "source": [
398
+ "## 9. Teardown\n",
399
+ "\n",
400
+ "Run this to shut down cleanly without ending the runtime."
401
+ ]
402
+ },
403
+ {
404
+ "cell_type": "code",
405
+ "execution_count": null,
406
+ "metadata": {},
407
+ "outputs": [],
408
+ "source": [
409
+ "if agent is not None:\n",
410
+ " agent.stop()\n",
411
+ "try:\n",
412
+ " server.terminate()\n",
413
+ "except Exception:\n",
414
+ " pass\n",
415
+ "print('Stopped.')"
416
+ ]
417
+ },
418
+ {
419
+ "cell_type": "markdown",
420
+ "metadata": {},
421
+ "source": [
422
+ "---\n",
423
+ "\n",
424
+ "## Troubleshooting\n",
425
+ "\n",
426
+ "| Symptom | Cause | Fix |\n",
427
+ "|---|---|---|\n",
428
+ "| `invalid_or_expired_pairing_token` | Join keys are single-use and time-limited. | Issue a new one in the portal. |\n",
429
+ "| `identity_public_key_already_registered` | This VM already paired; `/content/.thoxmesh` was kept. | Reuse it, or delete the folder to pair as a new device. |\n",
430
+ "| `Model endpoint must use a private...` | Registered a public URL via `device_v2`. | Use `THOX_REGISTRAR=spine`, or a private attach mode. |\n",
431
+ "| `gateway cannot reach` | Registered a private address via `spine`. | Start the tunnel, use `THOX_ATTACH_MODE=public`. |\n",
432
+ "| `looks like a Hugging Face token` | HF token pasted as the API key. | Use a portal API key with scope `mesh:register`. |\n",
433
+ "| `request_timestamp_out_of_window` | Runtime clock drifted >5 min. | Restart the runtime. |\n",
434
+ "| `replay_detected` | A nonce was reused. | Do not re-send a captured request; the agent handles this. |\n",
435
+ "\n",
436
+ "Security model: `templates/SECURITY.md`."
437
+ ]
438
+ }
439
+ ],
440
+ "metadata": {
441
+ "colab": {
442
+ "provenance": [],
443
+ "toc_visible": true,
444
+ "name": "THOX_Mesh_Node.ipynb"
445
+ },
446
+ "kernelspec": {
447
+ "display_name": "Python 3",
448
+ "name": "python3"
449
+ },
450
+ "language_info": {
451
+ "name": "python"
452
+ }
453
+ },
454
+ "nbformat": 4,
455
+ "nbformat_minor": 0
456
+ }