llmt-cli 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.
Files changed (57) hide show
  1. llmt_cli-0.2.0/LICENSE +21 -0
  2. llmt_cli-0.2.0/PKG-INFO +307 -0
  3. llmt_cli-0.2.0/README.md +281 -0
  4. llmt_cli-0.2.0/pyproject.toml +54 -0
  5. llmt_cli-0.2.0/setup.cfg +4 -0
  6. llmt_cli-0.2.0/src/llmt/__init__.py +7 -0
  7. llmt_cli-0.2.0/src/llmt/api.py +138 -0
  8. llmt_cli-0.2.0/src/llmt/cli.py +562 -0
  9. llmt_cli-0.2.0/src/llmt/clients.py +1316 -0
  10. llmt_cli-0.2.0/src/llmt/data/manifest-pubkey.json +7 -0
  11. llmt_cli-0.2.0/src/llmt/doctor.py +970 -0
  12. llmt_cli-0.2.0/src/llmt/doctorcmd.py +142 -0
  13. llmt_cli-0.2.0/src/llmt/download.py +321 -0
  14. llmt_cli-0.2.0/src/llmt/errors.py +132 -0
  15. llmt_cli-0.2.0/src/llmt/layout.py +676 -0
  16. llmt_cli-0.2.0/src/llmt/manifest.py +314 -0
  17. llmt_cli-0.2.0/src/llmt/matcher.py +358 -0
  18. llmt_cli-0.2.0/src/llmt/models.py +130 -0
  19. llmt_cli-0.2.0/src/llmt/output.py +51 -0
  20. llmt_cli-0.2.0/src/llmt/permissions.py +154 -0
  21. llmt_cli-0.2.0/src/llmt/scancmd.py +556 -0
  22. llmt_cli-0.2.0/src/llmt/scanner.py +328 -0
  23. llmt_cli-0.2.0/src/llmt/telemetry/__init__.py +15 -0
  24. llmt_cli-0.2.0/src/llmt/telemetry/artifacts.py +217 -0
  25. llmt_cli-0.2.0/src/llmt/telemetry/beat.py +221 -0
  26. llmt_cli-0.2.0/src/llmt/telemetry/cmd.py +358 -0
  27. llmt_cli-0.2.0/src/llmt/telemetry/collect.py +252 -0
  28. llmt_cli-0.2.0/src/llmt/telemetry/config.py +237 -0
  29. llmt_cli-0.2.0/src/llmt/telemetry/consent.py +57 -0
  30. llmt_cli-0.2.0/src/llmt/telemetry/errors.py +41 -0
  31. llmt_cli-0.2.0/src/llmt/telemetry/wire.py +194 -0
  32. llmt_cli-0.2.0/src/llmt/verify.py +129 -0
  33. llmt_cli-0.2.0/src/llmt_cli.egg-info/PKG-INFO +307 -0
  34. llmt_cli-0.2.0/src/llmt_cli.egg-info/SOURCES.txt +55 -0
  35. llmt_cli-0.2.0/src/llmt_cli.egg-info/dependency_links.txt +1 -0
  36. llmt_cli-0.2.0/src/llmt_cli.egg-info/entry_points.txt +2 -0
  37. llmt_cli-0.2.0/src/llmt_cli.egg-info/requires.txt +6 -0
  38. llmt_cli-0.2.0/src/llmt_cli.egg-info/top_level.txt +1 -0
  39. llmt_cli-0.2.0/tests/test_api_errors.py +65 -0
  40. llmt_cli-0.2.0/tests/test_clients.py +1596 -0
  41. llmt_cli-0.2.0/tests/test_doctor.py +466 -0
  42. llmt_cli-0.2.0/tests/test_doctor_sources.py +210 -0
  43. llmt_cli-0.2.0/tests/test_download.py +139 -0
  44. llmt_cli-0.2.0/tests/test_gate_d7.py +54 -0
  45. llmt_cli-0.2.0/tests/test_layout.py +425 -0
  46. llmt_cli-0.2.0/tests/test_live_smoke.py +68 -0
  47. llmt_cli-0.2.0/tests/test_manifest_auth.py +368 -0
  48. llmt_cli-0.2.0/tests/test_matcher.py +256 -0
  49. llmt_cli-0.2.0/tests/test_pull_verify.py +216 -0
  50. llmt_cli-0.2.0/tests/test_quirks.py +270 -0
  51. llmt_cli-0.2.0/tests/test_scan_integration.py +477 -0
  52. llmt_cli-0.2.0/tests/test_scan_multifile.py +473 -0
  53. llmt_cli-0.2.0/tests/test_scanner.py +169 -0
  54. llmt_cli-0.2.0/tests/test_search.py +39 -0
  55. llmt_cli-0.2.0/tests/test_supply_chain.py +352 -0
  56. llmt_cli-0.2.0/tests/test_telemetry.py +618 -0
  57. llmt_cli-0.2.0/tests/test_units.py +103 -0
llmt_cli-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LLM Torrents
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,307 @@
1
+ Metadata-Version: 2.4
2
+ Name: llmt-cli
3
+ Version: 0.2.0
4
+ Summary: Find, download (over BitTorrent + webseeds), and SHA-256-verify LLM models from the LLM Torrents catalog.
5
+ Author: LLM Torrents
6
+ License: MIT
7
+ Project-URL: Homepage, https://llmtorrents.com
8
+ Project-URL: Source, https://llmtorrents.com
9
+ Keywords: llm,torrent,bittorrent,webseed,sha256,ed25519,models,aria2
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Communications :: File Sharing
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests>=2.25
21
+ Requires-Dist: PyNaCl>=1.5
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
24
+ Requires-Dist: requests-mock>=1.9; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # llmt — LLM Torrents command-line client
28
+
29
+ `llmt` finds LLM models in the [LLM Torrents](https://llmtorrents.com) catalog,
30
+ downloads them over **BitTorrent with HTTP webseeds**, and — the point of the
31
+ whole thing — **verifies what you got**: it fetches the catalog's
32
+ **Ed25519-signed manifest** for the torrent, checks the signature against a
33
+ public key **pinned inside this package** (not fetched from the network), and
34
+ then verifies every downloaded file's SHA-256 against that verified manifest.
35
+ If a byte is wrong — or the manifest cannot be authenticated — `llmt` tells
36
+ you and exits non-zero.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install llmt-cli # from PyPI (once published; installs the `llmt` command)
42
+ # or, from a checkout:
43
+ pip install -e .
44
+ ```
45
+
46
+ Requires Python ≥ 3.9 and [`aria2c`](https://aria2.github.io/) on your `PATH`
47
+ for downloading/seeding (`apt install aria2`, `brew install aria2`, …). If
48
+ `aria2c` is missing, `llmt` prints a clear message instead of crashing.
49
+
50
+ ## Security & supply chain
51
+
52
+ `llmt` is high-trust software (it hashes your disk and drives a torrent
53
+ client), so it is built to be audited, not trusted:
54
+
55
+ - Catalog manifests are **Ed25519-signed**; `pull`/`verify`/`seed` verify the
56
+ signature against the public key **pinned in the package**
57
+ (`llmt/data/manifest-pubkey.json`) — the trust root ships with the code and
58
+ is never fetched from the network at runtime. A manifest that cannot be
59
+ authenticated (unreachable, malformed, bad signature, wrong/rotated key, or
60
+ disagreeing with the catalog) is **never reported as verified**: distinct
61
+ error, exit `15`. Key rotation ships as a new `llmt` release.
62
+ - `llmt permissions` prints exactly what the tool reads, writes, and sends
63
+ (no telemetry, **no self-update** — enforced by unit test). Prose version:
64
+ [docs/PERMISSIONS.md](docs/PERMISSIONS.md).
65
+ - Dependencies are pinned: `pip install llmt-cli -c constraints.txt`, or
66
+ fully hash-verified in two steps (the lock file covers the dependency
67
+ closure only — `llmt` itself is not in it, so install it separately):
68
+ `pip install --require-hashes -r requirements.lock && pip install --no-deps .`
69
+ - Releases ship a `SHA256SUMS` file (checksums published on
70
+ llmtorrents.com); build + signing process: [docs/RELEASING.md](docs/RELEASING.md).
71
+ - Vulnerability reports: [SECURITY.md](SECURITY.md).
72
+ - Docker image (pinned base, non-root) and homelab recipes:
73
+ [docs/recipes/](docs/recipes/).
74
+
75
+ ## Usage
76
+
77
+ ```bash
78
+ llmt search "llama" # readable table of matches
79
+ llmt search "qwen" --fit --rig vram_gb=24 # fit verdict for your rig
80
+ llmt search "phi" --json # machine-readable
81
+
82
+ llmt pull microsoft-phi-4-mini # best quant -> download -> verify
83
+ llmt pull microsoft-phi-4-mini:Q4_K_M # a specific quant
84
+ llmt pull acme-tiny-1b --dry-run # show the exact aria2c command
85
+
86
+ llmt verify ./acme-tiny-1b-Q4_K_M acme-tiny-1b:Q4_K_M # re-check, no download
87
+ llmt seed acme-tiny-1b:Q4_K_M ./acme-tiny-1b-Q4_K_M # help the swarm
88
+ ```
89
+
90
+ Global flags on every command: `--json`, `--api-base`, `--api-key`,
91
+ `--timeout`. Configure via env: `LLMT_API_BASE`, `LLMT_API_KEY`
92
+ (the complete list of every env var `llmt` reads is in
93
+ [docs/PERMISSIONS.md](docs/PERMISSIONS.md) and `llmt permissions`).
94
+ An API key is optional and only **raises your rate limits** — all reads are
95
+ public.
96
+
97
+ ## The trust story (signed manifest + SHA-256)
98
+
99
+ A torrent's own piece hashes only prove the bytes match *that* `.torrent`.
100
+ They don't prove the `.torrent` describes the model the catalog vouched for
101
+ — and TLS alone only proves who you *talked to*, not that the file list is
102
+ the one the catalog *signed*. `llmt` closes both gaps:
103
+
104
+ 1. **The Ed25519-signed manifest is the source of truth.** For a non-gated
105
+ model, `llmt` fetches the torrent's manifest
106
+ (`GET /api/manifests/{info_hash}`) and **verifies its detached Ed25519
107
+ signature** against the public key pinned inside this package
108
+ (`llmt/data/manifest-pubkey.json`, per the open
109
+ [manifest spec](https://llmtorrents.com/schema/manifest-v0.1.0.json)).
110
+ It also checks that the signed document actually names the torrent being
111
+ verified, and that the API's `download.files[]` block agrees with the
112
+ signed manifest.
113
+ 2. **After the download**, `llmt` streams each file through SHA-256 (64 KiB
114
+ chunks, no whole-file buffering) and compares byte-for-byte against the
115
+ **verified manifest's** hash. This is a real, independent check — not
116
+ aria2c's piece check.
117
+ 3. **Any mismatch, missing file, or wrong size fails the run** and `llmt`
118
+ exits non-zero (`3`). A tampered or truncated file cannot pass silently.
119
+ A file for which no hash was supplied is flagged `NO-HASH`, never waved
120
+ through.
121
+ 4. **An unauthenticatable manifest fails the run before any hashing** —
122
+ unreachable endpoint (e.g. offline), malformed document, invalid
123
+ signature, unknown/rotated signing key, wrong torrent, or a
124
+ catalog/manifest disagreement each produce a distinct error and exit
125
+ `15`. `llmt` never silently falls back to trusting unsigned hashes over
126
+ TLS. (If the key was rotated, upgrading to the newest `llmt` release
127
+ picks up the new pinned key.)
128
+ 5. **`llmt verify`** re-runs exactly this check on already-downloaded files
129
+ — including the manifest signature check, so it needs the catalog API to
130
+ be reachable; there is no offline "verified". `llmt seed` refuses to
131
+ seed a torrent whose manifest cannot be authenticated.
132
+
133
+ Because a wrong file trips a non-zero exit, `llmt pull` / `llmt verify` drop
134
+ straight into a pipeline: if it exits `0`, every file matched the
135
+ Ed25519-verified manifest.
136
+
137
+ ## License-gated models (why some models won't download)
138
+
139
+ Some models are distributed under a click-through license. For those, the
140
+ catalog API serves **no download artifacts at all** — no magnet, no hashes, no
141
+ file list. `llmt` detects this and tells you to accept the license on the
142
+ model's page first:
143
+
144
+ ```
145
+ $ llmt pull meta-llama-llama-3-3-70b-instruct
146
+ 'meta-llama-llama-3-3-70b-instruct' is license-gated. The catalog API does not
147
+ serve download artifacts for gated models.
148
+ Accept the license here, then retry: https://llmtorrents.com/models/meta-llama-llama-3-3-70b-instruct
149
+ ```
150
+
151
+ `llmt` will never fabricate a magnet or hash to work around a gate.
152
+
153
+ ## How the download works (aria2c + webseeds)
154
+
155
+ `llmt` delegates the transfer to `aria2c`, passing the catalog's magnet URI
156
+ plus every webseed URL as trailing HTTP URIs, so aria2c uses the project's
157
+ HTTP mirrors as web-seeds alongside the BitTorrent swarm. Roughly:
158
+
159
+ ```
160
+ aria2c --dir <out> --enable-dht=true --bt-enable-lpd=true \
161
+ --check-integrity=true --max-connection-per-server=8 --seed-time=0 \
162
+ [--bt-tracker <tracker> ...] \
163
+ <magnet> <webseed-url-1> <webseed-url-2> ...
164
+ ```
165
+
166
+ Use `llmt pull ... --dry-run` to print the exact command for your model.
167
+
168
+ ### Known webseed quirks (why aria2c may exit non-zero on a good download)
169
+
170
+ Both HTTP origins serve the model files but **403 the bare base URL**
171
+ (directory listings are disabled by design), and hybrid v1+v2 torrents
172
+ contain BEP-47 `.pad` entries that **404 on the HTTP mirrors** (often seen as
173
+ a "stall" at 98-99% in aria2c's progress summary). Either can make `aria2c`
174
+ exit non-zero (e.g. code 22) even though every payload byte arrived.
175
+
176
+ `llmt pull` handles this: when aria2c exits non-zero it checks that every
177
+ expected file is present on disk at its expected size. If so, it notes the
178
+ quirk on stderr and proceeds straight to the full SHA-256 verification — the
179
+ hash check against the signed manifest, not aria2c's exit code, is the trust
180
+ signal. If files are missing or the wrong size, the download really did fail
181
+ and `llmt` exits `10` loudly.
182
+
183
+ `llmt verify <dir> <ref>` accepts either the **outer** download directory
184
+ (the one `pull` created) or the **inner** payload directory the torrent
185
+ unpacked inside it — it detects which you passed. If it can't find the files
186
+ at all, it tells you exactly which directory to pass.
187
+
188
+ ## Seeding your existing models (the layout mapper)
189
+
190
+ SHA-256 tells you a file *is* the right bytes, but a BitTorrent client won't
191
+ seed those bytes unless they appear at the exact path the torrent expects,
192
+ under the torrent's root folder. Your model lives at
193
+ `/models/qwen3-30b/model.gguf`; the torrent wants
194
+ `qwen3-30b-a3b-q4-k-m/Qwen3-30B-A3B-Q4_K_M.gguf`. The layout mapper plans how to
195
+ present your existing file to the client **without moving or copying it**.
196
+
197
+ - **Direct-use:** if your file already sits at the expected relative path, the
198
+ plan is just "add the torrent with this save path" — no links at all.
199
+ - **Link farm (default for mismatched layouts):** the mapper builds a tiny tree
200
+ under `~/.llmt/seeding-layouts/<torrent-root>/…` whose entries are hard/sym
201
+ **links** back to your real file. Your model folder stays your model folder;
202
+ the farm is just a set of pointers the client can follow. Save path is the
203
+ layouts dir; the client rechecks, sees 100%, and seeds. Model weights are
204
+ never re-downloaded and nothing is duplicated on disk. One honest caveat for
205
+ multi-file torrents you hold only partially: small companion files
206
+ (license/README-class, at most 10 MiB total per torrent) may be fetched by
207
+ your client, and `llmt scan` prints a notice naming them and where they land
208
+ first; any larger missing file is marked "do not download" in your client
209
+ before the torrent can resume, with a warning that it will seed only what
210
+ you have and show as incomplete.
211
+
212
+ Hardlinks are preferred whenever the file and the farm are on the same
213
+ filesystem/volume — they need no privileges and every client follows them. A
214
+ symlink is the labelled cross-volume fallback.
215
+
216
+ | Platform | Same volume | Cross volume |
217
+ | --- | --- | --- |
218
+ | Linux / macOS | hardlink | symlink |
219
+ | Windows | hardlink (same drive, no admin) | symlink — needs **Administrator or Developer Mode** (NTFS junctions redirect directories only, not files) |
220
+
221
+ The client must **force-recheck** after adding so it verifies the linked bytes
222
+ and reaches 100%. The mapper refuses any path that would escape the layouts dir
223
+ (`..`, absolute/drive-letter/UNC tricks, reserved names, null bytes) and never
224
+ silently replaces an existing link that points somewhere else.
225
+
226
+ **Dockerized or remote clients (seedbox / unRAID):** a qBittorrent or
227
+ Transmission running in a container only sees its own **mounted volumes**, not
228
+ host paths like `~/.llmt/seeding-layouts`. If the link farm lives on a path the
229
+ client cannot see, its force-recheck reads **0%** and `llmt scan` **fails
230
+ closed** (it never resumes an unverified torrent). Point the farm at a directory
231
+ the client has mounted with `--layout-base`, e.g.
232
+ `llmt scan --layout-base /mnt/user/appdata/qbittorrent/llmt-layouts`. `llmt
233
+ scan` also prints an early advisory when it detects the client's own default
234
+ save path does not exist on the machine llmt is running on (a sign the two are
235
+ on different filesystems), and `llmt doctor` reports a `layout-base-visible`
236
+ check that WARNs in the same situation.
237
+
238
+ ## Opt-in seeding telemetry (off by default)
239
+
240
+ Telemetry lets the LLM Torrents site **credit you for the artifacts you
241
+ seed**. It is a *separate, explicit, off-by-default, revocable* opt-in — it
242
+ never runs until you turn it on, and it never sends file paths or scan data.
243
+
244
+ ```bash
245
+ llmt telemetry enable # prints the exact consent statement, then
246
+ # prompts for your token (from the website)
247
+ llmt telemetry enable --yes --token llmta_xx… # non-interactive (scripts)
248
+ llmt telemetry status # consent state, token presence, last beat
249
+ llmt telemetry whoami # who the stored token belongs to (GET /me)
250
+ llmt telemetry beat --qb-url http://localhost:8080 # send one heartbeat
251
+ llmt telemetry beat --loop # keep beating every 15 min (or --interval N)
252
+ llmt telemetry disable # stop sending AND delete the stored token
253
+ ```
254
+
255
+ `beat` reads the torrents from your client (`--qb-url`/`--tr-url` or
256
+ `LLMT_QB_URL`/`LLMT_TR_URL`, same flags as `scan`/`doctor`), reports each
257
+ torrent's infohashes (v1 **and** v2 for hybrid torrents), name, state and
258
+ progress plus the SHA-256s of the artifacts you hold, and answers any
259
+ possession challenges the server issues by hashing a server-chosen byte range
260
+ of the local file. Run it from `cron` (the primary mode) or with `--loop`.
261
+
262
+ **What is sent:** torrent infohashes/names/states/progress, artifact
263
+ SHA-256s, and challenge byte-range hashes — nothing else.
264
+ **What is never sent:** file paths, filenames, scan data, or machine info.
265
+ The token is stored at `~/.llmt/agent-token` (mode `0600`) and is never
266
+ printed after it is saved. View or delete your server-side data on the
267
+ website (Account -> Telemetry). See [docs/PERMISSIONS.md](docs/PERMISSIONS.md)
268
+ and `llmt permissions` for the full statement.
269
+
270
+ Config keys (in `~/.llmt/config.json`, all under `telemetry`):
271
+ `enabled` (default `false`), `endpoint` (default `https://llmtorrents.com`),
272
+ `interval_minutes` (`15`), `max_torrents_per_heartbeat` (`500`),
273
+ `report_artifacts` (`true`), `token_path`, `challenge_chunk_size`.
274
+ Override the endpoint per-run with `--endpoint` or `LLMT_TELEMETRY_ENDPOINT`.
275
+
276
+ ## Exit codes
277
+
278
+ | code | meaning |
279
+ |------|---------|
280
+ | 0 | success / all files verified |
281
+ | 2 | generic API error (incl. 403) |
282
+ | 3 | **SHA-256 verification failed** |
283
+ | 4 | not found (404) |
284
+ | 5 | rate limited (429; respects `Retry-After`) |
285
+ | 6 | request timed out |
286
+ | 7 | model is license-gated (D7) |
287
+ | 8 | no gate-passed torrent available |
288
+ | 9 | `aria2c` not installed |
289
+ | 10 | `aria2c` failed and the download is incomplete |
290
+ | 14 | torrent-client add/connection failed |
291
+ | 15 | **manifest unverifiable** (fetch failed/offline, malformed, bad signature, wrong/rotated key, wrong torrent, or catalog/manifest mismatch) — nothing was verified |
292
+ | 20 | telemetry error (generic / disabled) |
293
+ | 21 | telemetry token missing/unknown/revoked (401) |
294
+ | 22 | telemetry token lacks `telemetry:write` scope (403) |
295
+ | 23 | telemetry config error (no token / bad endpoint) |
296
+
297
+ ## Development
298
+
299
+ ```bash
300
+ pip install -e ".[dev]"
301
+ pytest -m "not live" # offline unit suite (mocked API)
302
+ pytest -m live # read-only smoke calls to the real API
303
+ ```
304
+
305
+ ## License
306
+
307
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,281 @@
1
+ # llmt — LLM Torrents command-line client
2
+
3
+ `llmt` finds LLM models in the [LLM Torrents](https://llmtorrents.com) catalog,
4
+ downloads them over **BitTorrent with HTTP webseeds**, and — the point of the
5
+ whole thing — **verifies what you got**: it fetches the catalog's
6
+ **Ed25519-signed manifest** for the torrent, checks the signature against a
7
+ public key **pinned inside this package** (not fetched from the network), and
8
+ then verifies every downloaded file's SHA-256 against that verified manifest.
9
+ If a byte is wrong — or the manifest cannot be authenticated — `llmt` tells
10
+ you and exits non-zero.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install llmt-cli # from PyPI (once published; installs the `llmt` command)
16
+ # or, from a checkout:
17
+ pip install -e .
18
+ ```
19
+
20
+ Requires Python ≥ 3.9 and [`aria2c`](https://aria2.github.io/) on your `PATH`
21
+ for downloading/seeding (`apt install aria2`, `brew install aria2`, …). If
22
+ `aria2c` is missing, `llmt` prints a clear message instead of crashing.
23
+
24
+ ## Security & supply chain
25
+
26
+ `llmt` is high-trust software (it hashes your disk and drives a torrent
27
+ client), so it is built to be audited, not trusted:
28
+
29
+ - Catalog manifests are **Ed25519-signed**; `pull`/`verify`/`seed` verify the
30
+ signature against the public key **pinned in the package**
31
+ (`llmt/data/manifest-pubkey.json`) — the trust root ships with the code and
32
+ is never fetched from the network at runtime. A manifest that cannot be
33
+ authenticated (unreachable, malformed, bad signature, wrong/rotated key, or
34
+ disagreeing with the catalog) is **never reported as verified**: distinct
35
+ error, exit `15`. Key rotation ships as a new `llmt` release.
36
+ - `llmt permissions` prints exactly what the tool reads, writes, and sends
37
+ (no telemetry, **no self-update** — enforced by unit test). Prose version:
38
+ [docs/PERMISSIONS.md](docs/PERMISSIONS.md).
39
+ - Dependencies are pinned: `pip install llmt-cli -c constraints.txt`, or
40
+ fully hash-verified in two steps (the lock file covers the dependency
41
+ closure only — `llmt` itself is not in it, so install it separately):
42
+ `pip install --require-hashes -r requirements.lock && pip install --no-deps .`
43
+ - Releases ship a `SHA256SUMS` file (checksums published on
44
+ llmtorrents.com); build + signing process: [docs/RELEASING.md](docs/RELEASING.md).
45
+ - Vulnerability reports: [SECURITY.md](SECURITY.md).
46
+ - Docker image (pinned base, non-root) and homelab recipes:
47
+ [docs/recipes/](docs/recipes/).
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ llmt search "llama" # readable table of matches
53
+ llmt search "qwen" --fit --rig vram_gb=24 # fit verdict for your rig
54
+ llmt search "phi" --json # machine-readable
55
+
56
+ llmt pull microsoft-phi-4-mini # best quant -> download -> verify
57
+ llmt pull microsoft-phi-4-mini:Q4_K_M # a specific quant
58
+ llmt pull acme-tiny-1b --dry-run # show the exact aria2c command
59
+
60
+ llmt verify ./acme-tiny-1b-Q4_K_M acme-tiny-1b:Q4_K_M # re-check, no download
61
+ llmt seed acme-tiny-1b:Q4_K_M ./acme-tiny-1b-Q4_K_M # help the swarm
62
+ ```
63
+
64
+ Global flags on every command: `--json`, `--api-base`, `--api-key`,
65
+ `--timeout`. Configure via env: `LLMT_API_BASE`, `LLMT_API_KEY`
66
+ (the complete list of every env var `llmt` reads is in
67
+ [docs/PERMISSIONS.md](docs/PERMISSIONS.md) and `llmt permissions`).
68
+ An API key is optional and only **raises your rate limits** — all reads are
69
+ public.
70
+
71
+ ## The trust story (signed manifest + SHA-256)
72
+
73
+ A torrent's own piece hashes only prove the bytes match *that* `.torrent`.
74
+ They don't prove the `.torrent` describes the model the catalog vouched for
75
+ — and TLS alone only proves who you *talked to*, not that the file list is
76
+ the one the catalog *signed*. `llmt` closes both gaps:
77
+
78
+ 1. **The Ed25519-signed manifest is the source of truth.** For a non-gated
79
+ model, `llmt` fetches the torrent's manifest
80
+ (`GET /api/manifests/{info_hash}`) and **verifies its detached Ed25519
81
+ signature** against the public key pinned inside this package
82
+ (`llmt/data/manifest-pubkey.json`, per the open
83
+ [manifest spec](https://llmtorrents.com/schema/manifest-v0.1.0.json)).
84
+ It also checks that the signed document actually names the torrent being
85
+ verified, and that the API's `download.files[]` block agrees with the
86
+ signed manifest.
87
+ 2. **After the download**, `llmt` streams each file through SHA-256 (64 KiB
88
+ chunks, no whole-file buffering) and compares byte-for-byte against the
89
+ **verified manifest's** hash. This is a real, independent check — not
90
+ aria2c's piece check.
91
+ 3. **Any mismatch, missing file, or wrong size fails the run** and `llmt`
92
+ exits non-zero (`3`). A tampered or truncated file cannot pass silently.
93
+ A file for which no hash was supplied is flagged `NO-HASH`, never waved
94
+ through.
95
+ 4. **An unauthenticatable manifest fails the run before any hashing** —
96
+ unreachable endpoint (e.g. offline), malformed document, invalid
97
+ signature, unknown/rotated signing key, wrong torrent, or a
98
+ catalog/manifest disagreement each produce a distinct error and exit
99
+ `15`. `llmt` never silently falls back to trusting unsigned hashes over
100
+ TLS. (If the key was rotated, upgrading to the newest `llmt` release
101
+ picks up the new pinned key.)
102
+ 5. **`llmt verify`** re-runs exactly this check on already-downloaded files
103
+ — including the manifest signature check, so it needs the catalog API to
104
+ be reachable; there is no offline "verified". `llmt seed` refuses to
105
+ seed a torrent whose manifest cannot be authenticated.
106
+
107
+ Because a wrong file trips a non-zero exit, `llmt pull` / `llmt verify` drop
108
+ straight into a pipeline: if it exits `0`, every file matched the
109
+ Ed25519-verified manifest.
110
+
111
+ ## License-gated models (why some models won't download)
112
+
113
+ Some models are distributed under a click-through license. For those, the
114
+ catalog API serves **no download artifacts at all** — no magnet, no hashes, no
115
+ file list. `llmt` detects this and tells you to accept the license on the
116
+ model's page first:
117
+
118
+ ```
119
+ $ llmt pull meta-llama-llama-3-3-70b-instruct
120
+ 'meta-llama-llama-3-3-70b-instruct' is license-gated. The catalog API does not
121
+ serve download artifacts for gated models.
122
+ Accept the license here, then retry: https://llmtorrents.com/models/meta-llama-llama-3-3-70b-instruct
123
+ ```
124
+
125
+ `llmt` will never fabricate a magnet or hash to work around a gate.
126
+
127
+ ## How the download works (aria2c + webseeds)
128
+
129
+ `llmt` delegates the transfer to `aria2c`, passing the catalog's magnet URI
130
+ plus every webseed URL as trailing HTTP URIs, so aria2c uses the project's
131
+ HTTP mirrors as web-seeds alongside the BitTorrent swarm. Roughly:
132
+
133
+ ```
134
+ aria2c --dir <out> --enable-dht=true --bt-enable-lpd=true \
135
+ --check-integrity=true --max-connection-per-server=8 --seed-time=0 \
136
+ [--bt-tracker <tracker> ...] \
137
+ <magnet> <webseed-url-1> <webseed-url-2> ...
138
+ ```
139
+
140
+ Use `llmt pull ... --dry-run` to print the exact command for your model.
141
+
142
+ ### Known webseed quirks (why aria2c may exit non-zero on a good download)
143
+
144
+ Both HTTP origins serve the model files but **403 the bare base URL**
145
+ (directory listings are disabled by design), and hybrid v1+v2 torrents
146
+ contain BEP-47 `.pad` entries that **404 on the HTTP mirrors** (often seen as
147
+ a "stall" at 98-99% in aria2c's progress summary). Either can make `aria2c`
148
+ exit non-zero (e.g. code 22) even though every payload byte arrived.
149
+
150
+ `llmt pull` handles this: when aria2c exits non-zero it checks that every
151
+ expected file is present on disk at its expected size. If so, it notes the
152
+ quirk on stderr and proceeds straight to the full SHA-256 verification — the
153
+ hash check against the signed manifest, not aria2c's exit code, is the trust
154
+ signal. If files are missing or the wrong size, the download really did fail
155
+ and `llmt` exits `10` loudly.
156
+
157
+ `llmt verify <dir> <ref>` accepts either the **outer** download directory
158
+ (the one `pull` created) or the **inner** payload directory the torrent
159
+ unpacked inside it — it detects which you passed. If it can't find the files
160
+ at all, it tells you exactly which directory to pass.
161
+
162
+ ## Seeding your existing models (the layout mapper)
163
+
164
+ SHA-256 tells you a file *is* the right bytes, but a BitTorrent client won't
165
+ seed those bytes unless they appear at the exact path the torrent expects,
166
+ under the torrent's root folder. Your model lives at
167
+ `/models/qwen3-30b/model.gguf`; the torrent wants
168
+ `qwen3-30b-a3b-q4-k-m/Qwen3-30B-A3B-Q4_K_M.gguf`. The layout mapper plans how to
169
+ present your existing file to the client **without moving or copying it**.
170
+
171
+ - **Direct-use:** if your file already sits at the expected relative path, the
172
+ plan is just "add the torrent with this save path" — no links at all.
173
+ - **Link farm (default for mismatched layouts):** the mapper builds a tiny tree
174
+ under `~/.llmt/seeding-layouts/<torrent-root>/…` whose entries are hard/sym
175
+ **links** back to your real file. Your model folder stays your model folder;
176
+ the farm is just a set of pointers the client can follow. Save path is the
177
+ layouts dir; the client rechecks, sees 100%, and seeds. Model weights are
178
+ never re-downloaded and nothing is duplicated on disk. One honest caveat for
179
+ multi-file torrents you hold only partially: small companion files
180
+ (license/README-class, at most 10 MiB total per torrent) may be fetched by
181
+ your client, and `llmt scan` prints a notice naming them and where they land
182
+ first; any larger missing file is marked "do not download" in your client
183
+ before the torrent can resume, with a warning that it will seed only what
184
+ you have and show as incomplete.
185
+
186
+ Hardlinks are preferred whenever the file and the farm are on the same
187
+ filesystem/volume — they need no privileges and every client follows them. A
188
+ symlink is the labelled cross-volume fallback.
189
+
190
+ | Platform | Same volume | Cross volume |
191
+ | --- | --- | --- |
192
+ | Linux / macOS | hardlink | symlink |
193
+ | Windows | hardlink (same drive, no admin) | symlink — needs **Administrator or Developer Mode** (NTFS junctions redirect directories only, not files) |
194
+
195
+ The client must **force-recheck** after adding so it verifies the linked bytes
196
+ and reaches 100%. The mapper refuses any path that would escape the layouts dir
197
+ (`..`, absolute/drive-letter/UNC tricks, reserved names, null bytes) and never
198
+ silently replaces an existing link that points somewhere else.
199
+
200
+ **Dockerized or remote clients (seedbox / unRAID):** a qBittorrent or
201
+ Transmission running in a container only sees its own **mounted volumes**, not
202
+ host paths like `~/.llmt/seeding-layouts`. If the link farm lives on a path the
203
+ client cannot see, its force-recheck reads **0%** and `llmt scan` **fails
204
+ closed** (it never resumes an unverified torrent). Point the farm at a directory
205
+ the client has mounted with `--layout-base`, e.g.
206
+ `llmt scan --layout-base /mnt/user/appdata/qbittorrent/llmt-layouts`. `llmt
207
+ scan` also prints an early advisory when it detects the client's own default
208
+ save path does not exist on the machine llmt is running on (a sign the two are
209
+ on different filesystems), and `llmt doctor` reports a `layout-base-visible`
210
+ check that WARNs in the same situation.
211
+
212
+ ## Opt-in seeding telemetry (off by default)
213
+
214
+ Telemetry lets the LLM Torrents site **credit you for the artifacts you
215
+ seed**. It is a *separate, explicit, off-by-default, revocable* opt-in — it
216
+ never runs until you turn it on, and it never sends file paths or scan data.
217
+
218
+ ```bash
219
+ llmt telemetry enable # prints the exact consent statement, then
220
+ # prompts for your token (from the website)
221
+ llmt telemetry enable --yes --token llmta_xx… # non-interactive (scripts)
222
+ llmt telemetry status # consent state, token presence, last beat
223
+ llmt telemetry whoami # who the stored token belongs to (GET /me)
224
+ llmt telemetry beat --qb-url http://localhost:8080 # send one heartbeat
225
+ llmt telemetry beat --loop # keep beating every 15 min (or --interval N)
226
+ llmt telemetry disable # stop sending AND delete the stored token
227
+ ```
228
+
229
+ `beat` reads the torrents from your client (`--qb-url`/`--tr-url` or
230
+ `LLMT_QB_URL`/`LLMT_TR_URL`, same flags as `scan`/`doctor`), reports each
231
+ torrent's infohashes (v1 **and** v2 for hybrid torrents), name, state and
232
+ progress plus the SHA-256s of the artifacts you hold, and answers any
233
+ possession challenges the server issues by hashing a server-chosen byte range
234
+ of the local file. Run it from `cron` (the primary mode) or with `--loop`.
235
+
236
+ **What is sent:** torrent infohashes/names/states/progress, artifact
237
+ SHA-256s, and challenge byte-range hashes — nothing else.
238
+ **What is never sent:** file paths, filenames, scan data, or machine info.
239
+ The token is stored at `~/.llmt/agent-token` (mode `0600`) and is never
240
+ printed after it is saved. View or delete your server-side data on the
241
+ website (Account -> Telemetry). See [docs/PERMISSIONS.md](docs/PERMISSIONS.md)
242
+ and `llmt permissions` for the full statement.
243
+
244
+ Config keys (in `~/.llmt/config.json`, all under `telemetry`):
245
+ `enabled` (default `false`), `endpoint` (default `https://llmtorrents.com`),
246
+ `interval_minutes` (`15`), `max_torrents_per_heartbeat` (`500`),
247
+ `report_artifacts` (`true`), `token_path`, `challenge_chunk_size`.
248
+ Override the endpoint per-run with `--endpoint` or `LLMT_TELEMETRY_ENDPOINT`.
249
+
250
+ ## Exit codes
251
+
252
+ | code | meaning |
253
+ |------|---------|
254
+ | 0 | success / all files verified |
255
+ | 2 | generic API error (incl. 403) |
256
+ | 3 | **SHA-256 verification failed** |
257
+ | 4 | not found (404) |
258
+ | 5 | rate limited (429; respects `Retry-After`) |
259
+ | 6 | request timed out |
260
+ | 7 | model is license-gated (D7) |
261
+ | 8 | no gate-passed torrent available |
262
+ | 9 | `aria2c` not installed |
263
+ | 10 | `aria2c` failed and the download is incomplete |
264
+ | 14 | torrent-client add/connection failed |
265
+ | 15 | **manifest unverifiable** (fetch failed/offline, malformed, bad signature, wrong/rotated key, wrong torrent, or catalog/manifest mismatch) — nothing was verified |
266
+ | 20 | telemetry error (generic / disabled) |
267
+ | 21 | telemetry token missing/unknown/revoked (401) |
268
+ | 22 | telemetry token lacks `telemetry:write` scope (403) |
269
+ | 23 | telemetry config error (no token / bad endpoint) |
270
+
271
+ ## Development
272
+
273
+ ```bash
274
+ pip install -e ".[dev]"
275
+ pytest -m "not live" # offline unit suite (mocked API)
276
+ pytest -m live # read-only smoke calls to the real API
277
+ ```
278
+
279
+ ## License
280
+
281
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "llmt-cli"
7
+ version = "0.2.0"
8
+ description = "Find, download (over BitTorrent + webseeds), and SHA-256-verify LLM models from the LLM Torrents catalog."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "LLM Torrents" }]
13
+ keywords = ["llm", "torrent", "bittorrent", "webseed", "sha256", "ed25519", "models", "aria2"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Communications :: File Sharing",
21
+ "Topic :: Utilities",
22
+ ]
23
+ # PyNaCl provides the Ed25519 verify primitive for signed manifests (M2):
24
+ # it binds libsodium — the same library the catalog signs with
25
+ # (crypto_sign_detached) — and is the verifier the frozen manifest spec's
26
+ # reference example uses. Growing this list is a deliberate, reviewed act:
27
+ # see tests/test_supply_chain.py::test_runtime_deps_stay_minimal.
28
+ dependencies = [
29
+ "requests>=2.25",
30
+ "PyNaCl>=1.5",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = ["pytest>=7.0", "requests-mock>=1.9"]
35
+
36
+ [project.urls]
37
+ Homepage = "https://llmtorrents.com"
38
+ Source = "https://llmtorrents.com"
39
+
40
+ [project.scripts]
41
+ llmt = "llmt.cli:main"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.setuptools.package-data]
47
+ # The pinned manifest-signing public key (trust root for `llmt verify`).
48
+ llmt = ["data/*.json"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+ markers = [
53
+ "live: hits the real llmtorrents.com API (read-only smoke); run with -m live",
54
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """llmt — LLM Torrents command-line client.
2
+
3
+ Find, download (over BitTorrent + webseeds) and SHA-256-verify LLM models
4
+ from the public LLM Torrents catalog API.
5
+ """
6
+
7
+ __version__ = "0.2.0"