ollanet 0.4.1 → 0.6.0
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.
- package/README.md +95 -9
- package/dist/bench-suite.d.ts +2 -0
- package/dist/bench-suite.js +1 -0
- package/dist/bench.js +54 -18
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +52 -2
- package/dist/compare-store.d.ts +8 -0
- package/dist/compare-store.js +55 -0
- package/dist/compare.d.ts +52 -0
- package/dist/compare.js +338 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +9 -1
- package/dist/mcp.js +235 -3
- package/dist/ollama-chat.d.ts +46 -2
- package/dist/ollama-chat.js +121 -2
- package/dist/paths.d.ts +4 -0
- package/dist/paths.js +8 -0
- package/dist/prompt-input.d.ts +7 -0
- package/dist/prompt-input.js +36 -0
- package/dist/prompt.js +28 -5
- package/dist/ps.d.ts +31 -0
- package/dist/ps.js +165 -0
- package/dist/pull.d.ts +36 -0
- package/dist/pull.js +198 -0
- package/dist/rm.d.ts +22 -0
- package/dist/rm.js +134 -0
- package/dist/scan-store.d.ts +15 -0
- package/dist/scan-store.js +41 -0
- package/dist/scan.d.ts +14 -1
- package/dist/scan.js +76 -42
- package/dist/show.d.ts +28 -0
- package/dist/show.js +131 -0
- package/dist/target.d.ts +4 -0
- package/dist/target.js +18 -0
- package/dist/tuned.d.ts +8 -0
- package/dist/tuned.js +17 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ This started as a personal little script. I had Ollama running on a laptop on my
|
|
|
18
18
|
|
|
19
19
|
## Related: Finetuna
|
|
20
20
|
|
|
21
|
-
**ollanet** is the client-side / network tool. On the machine that *runs* Ollama, use **[Finetuna](https://
|
|
21
|
+
**ollanet** is the client-side / network tool. On the machine that *runs* Ollama, use **[Finetuna](https://finetuna.net)** to turn a stock model into a GPU-tuned, named variant (context, batch, Modelfile, auto-tune).
|
|
22
22
|
|
|
23
23
|
Typical loop:
|
|
24
24
|
|
|
@@ -30,11 +30,14 @@ They share the same Ollama API; Finetuna shapes the models, ollanet finds and ch
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
32
32
|
- **Discover** Ollama hosts from localhost, `config.hosts`, `OLLANET_HOSTS`, optional Tailscale, and optional LAN scan (`--lan`, off by default)
|
|
33
|
-
- **
|
|
33
|
+
- **Pull** a library model onto a named host (`ollanet pull studio gemma3:12b`) — the server downloads; re-pull updates
|
|
34
|
+
- **Show / rm / ps** — inspect a Modelfile, delete a model (`--yes`), see what’s in VRAM. Scan marks Finetuna-style `[tuned]` names
|
|
35
|
+
- **Prompt** any hostname/IP/model with streaming replies (`--file` for `.txt` / `.md`)
|
|
36
|
+
- **Compare** 2–5 models on one host with the same prompt; summary table + `compares/<id>.md`
|
|
34
37
|
- **Persist** chats as `responses/<hash>.json` with topic, machine, model, timestamps
|
|
35
38
|
- **Continue** any thread with `--chat <hash>`
|
|
36
39
|
- **Configure** per-machine defaults (model, temperature, context, …)
|
|
37
|
-
- **MCP** — `ollanet mcp` exposes scan/prompt/chats as Model Context Protocol tools for agents
|
|
40
|
+
- **MCP** — `ollanet mcp` exposes scan/prompt/compare/pull/show/rm/ps/chats as Model Context Protocol tools for agents
|
|
38
41
|
- **Library** — `import { scanNetwork } from "ollanet"` for apps (Node 20+, not the browser)
|
|
39
42
|
|
|
40
43
|
## Use cases
|
|
@@ -44,11 +47,12 @@ The core idea: turn “I have to remember which IP has which models loaded” in
|
|
|
44
47
|
**For agents and automation** — prefer `ollanet mcp` (stdio MCP). The same flows also work via CLI flags:
|
|
45
48
|
|
|
46
49
|
- **Live model inventory / routing** — `ollanet_scan` (or `scan --json`) lists reachable hosts and models. Route each sub-task to the best host: the big-VRAM box for reasoning, the laptop for quick lookups.
|
|
50
|
+
- **Put a new model on a box** — `ollanet_pull` (or `pull studio gemma3:12b`) asks that host to download from the registry. Re-pull updates. Then `show` / `ps` / `rm` manage what’s on that disk and in VRAM.
|
|
47
51
|
- **Conversations that survive machines** — chats are stored by short hash with full history. Start a long thread on the GPU box, continue it later with `ollanet_prompt` + `chat_id` (or `--chat <hash>`).
|
|
48
52
|
- **A “talk to any Ollama on my network” tool** — `ollanet_prompt` / `prompt … --json --no-stream`; no hard-coded endpoints.
|
|
49
53
|
- **Embed in an app** — same scan + pick + call as a library. The app does not own the GPUs; it discovers them (FilePress, CLIs, Electron, a local companion for a hosted product).
|
|
50
54
|
- **Fleet health and speed checks** — periodic `ollanet bench <host> --json` builds a record of which model on which machine is currently fastest (or has silently gone offline). Saved results live in `benchmarks/`.
|
|
51
|
-
- **The Finetuna loop** — [Finetuna](https://
|
|
55
|
+
- **The Finetuna loop** — [Finetuna](https://finetuna.net) shapes a GPU-tuned named variant on the host; ollanet `scan` / `show` / `prompt` it from anywhere on the network. After `pull`, ollanet prints the next step (`finetuna --model …`).
|
|
52
56
|
|
|
53
57
|
**For humans:**
|
|
54
58
|
|
|
@@ -115,8 +119,15 @@ ollanet scan
|
|
|
115
119
|
# Also TCP-scan your local /24s for open Ollama ports
|
|
116
120
|
ollanet scan --lan
|
|
117
121
|
|
|
122
|
+
# Put a library model on a named machine (that host downloads it)
|
|
123
|
+
ollanet pull studio gemma3:12b
|
|
124
|
+
ollanet show studio gemma3:12b
|
|
125
|
+
ollanet ps studio
|
|
126
|
+
|
|
118
127
|
# Talk to a machine by name, MagicDNS name, or IP
|
|
119
128
|
ollanet prompt localhost "What is MagicDNS?"
|
|
129
|
+
ollanet prompt localhost --file ./notes.md
|
|
130
|
+
ollanet compare studio gemma3:12b llama3.2:3b --prompt "Explain MagicDNS"
|
|
120
131
|
ollanet prompt 192.168.1.50 gemma4:12b "Hello from the LAN"
|
|
121
132
|
|
|
122
133
|
# Continue later
|
|
@@ -146,6 +157,7 @@ for (const s of servers) {
|
|
|
146
157
|
|---|---|---|
|
|
147
158
|
| `scanNetwork()` | localhost + config + `OLLANET_HOSTS` + Tailscale | Button / first “find servers” |
|
|
148
159
|
| `scanNetwork({ lanScan: true })` | plus TCP sweep of local `/24`s on port 11434 | Checkbox; never the default scan, never on every health check |
|
|
160
|
+
| `lastScan()` | last saved snapshot, no network | UI “show last inventory” |
|
|
149
161
|
| `discoverHosts()` then probe yourself | candidates only, no `/api/tags` | If the app already has its own Ollama client |
|
|
150
162
|
|
|
151
163
|
Pass `config` to skip the file (useful when the app already has its own config):
|
|
@@ -162,7 +174,9 @@ await scanNetwork({
|
|
|
162
174
|
|
|
163
175
|
`runPrompt({ save: false, writeStdout: false })` is a one-shot call that does not write `~/.ollanet/responses/`.
|
|
164
176
|
|
|
165
|
-
|
|
177
|
+
`pullModel({ machine, model })` asks that host to download (or update) a library model. The bits never transit ollanet. `showModel`, `removeModel({ yes: true })`, and `listLoaded` are the inspect / delete / VRAM trio.
|
|
178
|
+
|
|
179
|
+
Requires **ollanet ≥ 0.4.0** (library). **Model management (`pull` / `show` / `rm` / `ps`) needs ≥ 0.5.0.** **Compare, `lastScan`, and bench `--hot` need ≥ 0.6.0.** Bundlers (Vite / SvelteKit SSR):
|
|
166
180
|
|
|
167
181
|
```ts
|
|
168
182
|
// vite.config.ts
|
|
@@ -175,7 +189,12 @@ optimizeDeps: { exclude: ["ollanet"] },
|
|
|
175
189
|
| Command | Description |
|
|
176
190
|
|---|---|
|
|
177
191
|
| `ollanet scan` | Probe known/discovered hosts for Ollama + list models |
|
|
178
|
-
| `ollanet
|
|
192
|
+
| `ollanet pull <machine> <model>` | Ask that host to download / update a library model |
|
|
193
|
+
| `ollanet show <machine> <model>` | Inspect Modelfile / params / capabilities (`[tuned]` when it looks like Finetuna) |
|
|
194
|
+
| `ollanet rm <machine> <model> --yes` | Delete a model from that host’s disk |
|
|
195
|
+
| `ollanet ps [machine]` | Models loaded in VRAM (omit machine = every discovered host) |
|
|
196
|
+
| `ollanet prompt …` | Send a prompt / continue a chat (`--file` for `.txt` / `.md`) |
|
|
197
|
+
| `ollanet compare …` | Same prompt on 2–5 models; summary + `compares/<id>.md` |
|
|
179
198
|
| `ollanet chats` | List or inspect saved transcripts |
|
|
180
199
|
| `ollanet bench …` | Benchmark models for tok/s + lightweight quality checks |
|
|
181
200
|
| `ollanet mcp` | Stdio [MCP](https://modelcontextprotocol.io) server for agents |
|
|
@@ -185,6 +204,7 @@ optimizeDeps: { exclude: ["ollanet"] },
|
|
|
185
204
|
- `--json` — machine-readable output
|
|
186
205
|
- `--all` — also probe offline Tailscale peers
|
|
187
206
|
- `--lan` — scan local LAN CIDRs for open Ollama ports
|
|
207
|
+
- `--last` / `--cached` — print the last saved scan (no network). Written to `~/.ollanet/last-scan.json` when installed, or `./last-scan.json` in a checkout (`OLLANET_LAST_SCAN` overrides).
|
|
188
208
|
|
|
189
209
|
### `prompt` options
|
|
190
210
|
|
|
@@ -205,12 +225,70 @@ ollanet prompt --chat <hash> <prompt...>
|
|
|
205
225
|
| `--keep-alive <value>` | Keep model loaded (`5m`, `0`, `-1`, …) |
|
|
206
226
|
| `--format json\|<schema>` | JSON mode / schema |
|
|
207
227
|
| `--think` / `--no-think` | Enable/disable model thinking (default **off** so replies aren’t empty on qwen3-style models) |
|
|
228
|
+
| `--file <path>` | Prompt from a `.txt` or `.md` file (joins argv / stdin) |
|
|
208
229
|
| `--no-stream` | Buffer the full reply |
|
|
209
230
|
| `--no-save` | Do not write a transcript |
|
|
210
231
|
| `--json` | Emit JSON (includes chat id when saved) |
|
|
211
232
|
|
|
212
233
|
Machine can be a discovered name, hostname, FQDN, or IP (`192.168.1.50`, `host:11434`). Direct addresses work even if they were never scanned.
|
|
213
234
|
|
|
235
|
+
### `compare` options
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
ollanet compare <machine> <model> <model> [model...]
|
|
239
|
+
ollanet compare <machine> <model> <model> [model...] --prompt <text>
|
|
240
|
+
ollanet compare <machine> <model> <model> [model...] --file <path.txt|.md>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Same prompt on **2–5 models** on one host. Prints a tok/s table and writes `compares/<id>.md` (readable) plus `.json`. This is not `bench` (no fixed suite / repeats). Omit `--prompt` / `--file` to use the built-in mesh-host tasting prompt (printed on stderr when used).
|
|
244
|
+
|
|
245
|
+
| Flag | Meaning |
|
|
246
|
+
|---|---|
|
|
247
|
+
| `--prompt` / `-p` | Prompt text (default: built-in) |
|
|
248
|
+
| `--file` / `-f` | Prompt from `.txt` or `.md` |
|
|
249
|
+
| `--system` | System prompt |
|
|
250
|
+
| `--temperature` / `--num-predict` / `--num-ctx` / `--keep-alive` | Same as `prompt` |
|
|
251
|
+
| `--think` / `--no-think` | Thinking (default off) |
|
|
252
|
+
| `--unload` | Unload each model before the next (fairer tok/s) |
|
|
253
|
+
| `--no-save` | Do not write `compares/*` |
|
|
254
|
+
| `--json` | Full record on stdout |
|
|
255
|
+
|
|
256
|
+
Override dir with `OLLANET_COMPARES_DIR`.
|
|
257
|
+
|
|
258
|
+
### `pull` options
|
|
259
|
+
|
|
260
|
+
```text
|
|
261
|
+
ollanet pull <machine|ip> <model>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The named machine downloads from the Ollama registry onto **its** disk. Re-pulling the same name updates it. This is not a file upload and not a Finetuna runtime tune.
|
|
265
|
+
|
|
266
|
+
| Flag | Meaning |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `--machine <name>` | Host (same resolution as `prompt`) |
|
|
269
|
+
| `--model <name>` | Model to pull |
|
|
270
|
+
| `--insecure` | Allow HTTP / self-signed registries |
|
|
271
|
+
| `--no-stream` | Single final response (no progress chunks) |
|
|
272
|
+
| `--json` | Result JSON on stdout |
|
|
273
|
+
|
|
274
|
+
Timeout default is none (large pulls can take hours). Override with `OLLAMA_PULL_TIMEOUT_MS`. After a successful pull, stderr points at [Finetuna](https://finetuna.net) for a host-side named variant.
|
|
275
|
+
|
|
276
|
+
### `show` / `rm` / `ps`
|
|
277
|
+
|
|
278
|
+
```text
|
|
279
|
+
ollanet show <machine|ip> <model>
|
|
280
|
+
ollanet rm <machine|ip> <model> --yes
|
|
281
|
+
ollanet ps [machine]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
| Flag | Meaning |
|
|
285
|
+
|---|---|
|
|
286
|
+
| `--yes` / `-y` | Required for `rm` unless you confirm on a TTY |
|
|
287
|
+
| `--json` | Result JSON on stdout |
|
|
288
|
+
| `--machine` / `--model` | Same host resolution as `prompt` |
|
|
289
|
+
|
|
290
|
+
`scan` is on-disk inventory and marks `*-ctx32k` / `*-flash` / `*finetuna*` as `[tuned]`. `ps` is what’s resident. `show` reads the Modelfile so you can see a Finetuna bake from another machine.
|
|
291
|
+
|
|
214
292
|
### `chats` options
|
|
215
293
|
|
|
216
294
|
- `--json` — list summary JSON
|
|
@@ -226,8 +304,13 @@ Runs a **stdio MCP server** (no extra npm deps). Cursor / Claude Desktop / any M
|
|
|
226
304
|
|
|
227
305
|
| Tool | What it does |
|
|
228
306
|
|---|---|
|
|
229
|
-
| `ollanet_scan` | Discover hosts + models (`lan`, `all` optional) |
|
|
230
|
-
| `ollanet_prompt` | Prompt a host or continue with `chat_id` |
|
|
307
|
+
| `ollanet_scan` | Discover hosts + models (`lan`, `all`, `last` optional) |
|
|
308
|
+
| `ollanet_prompt` | Prompt a host or continue with `chat_id` (`file` optional) |
|
|
309
|
+
| `ollanet_compare` | Same prompt on 2–5 models (`machine`, `models`; `prompt` / `file` optional) |
|
|
310
|
+
| `ollanet_pull` | Pull / update a model on a host (`machine`, `model`) |
|
|
311
|
+
| `ollanet_show` | Inspect a model (`machine`, `model`) |
|
|
312
|
+
| `ollanet_rm` | Delete a model (`machine`, `model`, `confirm: true`) |
|
|
313
|
+
| `ollanet_ps` | Loaded models (`machine` optional) |
|
|
231
314
|
| `ollanet_list_chats` | Summarize saved transcripts |
|
|
232
315
|
| `ollanet_get_chat` | Load one chat (full history) |
|
|
233
316
|
|
|
@@ -252,11 +335,14 @@ Or, from a checkout: `"command": "pnpm"`, `"args": ["ollanet", "--", "mcp"]` (wi
|
|
|
252
335
|
ollanet bench <machine|ip> [model...]
|
|
253
336
|
ollanet bench <machine|ip> --all
|
|
254
337
|
ollanet bench localhost gemma4:12b --runs 5 --cold-load --json
|
|
338
|
+
ollanet bench localhost gemma4:12b --hot --runs 5
|
|
255
339
|
```
|
|
256
340
|
|
|
257
341
|
Runs a built-in suite (`quick` default, or `--suite full`) against one or more **completion**
|
|
258
342
|
models: instruction checks once, throughput case repeated (`--runs`, default 3) with
|
|
259
|
-
pinned `num_predict` / `seed` / `temperature`. Reports median tok/s + spread.
|
|
343
|
+
pinned `num_predict` / `seed` / `temperature`. Reports median tok/s + spread.
|
|
344
|
+
`--hot` discards the first throughput run and leaves models loaded so the counted
|
|
345
|
+
repeats are already warm (Finetuna steady-state). Under `--all`,
|
|
260
346
|
skips non-completion models (embeddings); multimodal `completion`+`vision` models stay in
|
|
261
347
|
(early-stop / pass_rate handle text-suite failures). Use `--exclude-vision` to drop them.
|
|
262
348
|
Saves `benchmarks/<id>.json` (or `~/.ollanet/benchmarks/` when installed).
|
package/dist/bench-suite.d.ts
CHANGED
|
@@ -21,5 +21,7 @@ export declare function comparabilityKey(opts: {
|
|
|
21
21
|
temperature: number;
|
|
22
22
|
think: boolean;
|
|
23
23
|
numCtx: number | null;
|
|
24
|
+
/** Steady-state tok/s: first throughput run discarded, no inter-model unload. */
|
|
25
|
+
hot?: boolean;
|
|
24
26
|
}): string;
|
|
25
27
|
export declare function median(values: number[]): number | undefined;
|
package/dist/bench-suite.js
CHANGED
|
@@ -134,6 +134,7 @@ export function comparabilityKey(opts) {
|
|
|
134
134
|
`temperature=${opts.temperature}`,
|
|
135
135
|
`think=${opts.think ? 1 : 0}`,
|
|
136
136
|
`num_ctx=${opts.numCtx ?? ""}`,
|
|
137
|
+
...(opts.hot ? ["hot=1"] : []),
|
|
137
138
|
].join("|");
|
|
138
139
|
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
139
140
|
}
|
package/dist/bench.js
CHANGED
|
@@ -16,11 +16,16 @@ function usage() {
|
|
|
16
16
|
ollanet bench <machine|ip> [model...] [options]
|
|
17
17
|
ollanet bench <machine|ip> --all [options]
|
|
18
18
|
|
|
19
|
+
Examples:
|
|
20
|
+
ollanet bench studio gemma3:12b --runs 5
|
|
21
|
+
ollanet bench studio gemma3:12b --hot --runs 5
|
|
22
|
+
|
|
19
23
|
Options:
|
|
20
24
|
--all Every completion-capable model from /api/tags
|
|
21
25
|
--exclude-vision With --all, skip models that advertise vision
|
|
22
26
|
--suite quick|full Prompt suite (default quick)
|
|
23
27
|
--runs <n> Throughput repeats (default 3)
|
|
28
|
+
--hot Discard first throughput run; keep models loaded
|
|
24
29
|
--warmup / --no-warmup
|
|
25
30
|
--cold-load Measure cold load via unload + /api/ps
|
|
26
31
|
--num-predict <n> Pin throughput length (default 256)
|
|
@@ -85,6 +90,8 @@ function parseArgs(argv) {
|
|
|
85
90
|
let all = false;
|
|
86
91
|
let excludeVision = false;
|
|
87
92
|
let warmup = true;
|
|
93
|
+
let warmupSet = false;
|
|
94
|
+
let hot = false;
|
|
88
95
|
let coldLoad = false;
|
|
89
96
|
let json = false;
|
|
90
97
|
let save = true;
|
|
@@ -126,10 +133,16 @@ function parseArgs(argv) {
|
|
|
126
133
|
}
|
|
127
134
|
if (arg === "--warmup") {
|
|
128
135
|
warmup = true;
|
|
136
|
+
warmupSet = true;
|
|
129
137
|
continue;
|
|
130
138
|
}
|
|
131
139
|
if (arg === "--no-warmup") {
|
|
132
140
|
warmup = false;
|
|
141
|
+
warmupSet = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (arg === "--hot") {
|
|
145
|
+
hot = true;
|
|
133
146
|
continue;
|
|
134
147
|
}
|
|
135
148
|
if (arg === "--cold-load") {
|
|
@@ -221,7 +234,8 @@ function parseArgs(argv) {
|
|
|
221
234
|
excludeVision,
|
|
222
235
|
suite,
|
|
223
236
|
runs,
|
|
224
|
-
warmup,
|
|
237
|
+
warmup: hot && !warmupSet ? false : warmup,
|
|
238
|
+
hot,
|
|
225
239
|
coldLoad,
|
|
226
240
|
json,
|
|
227
241
|
save,
|
|
@@ -382,12 +396,15 @@ async function runCaseAttempt(opts) {
|
|
|
382
396
|
};
|
|
383
397
|
}
|
|
384
398
|
}
|
|
399
|
+
function countedAttempts(attempts) {
|
|
400
|
+
return attempts.filter((a) => !a.discarded);
|
|
401
|
+
}
|
|
385
402
|
function summarizeModel(cases, cold, selfJudge) {
|
|
386
403
|
const throughput = cases.find((c) => c.id === "throughput");
|
|
387
|
-
const
|
|
404
|
+
const counted = countedAttempts(throughput?.attempts ?? []);
|
|
405
|
+
const fullLen = counted.filter((a) => !a.error && !a.early_stop && a.tok_s != null);
|
|
388
406
|
const toks = fullLen.map((a) => a.tok_s);
|
|
389
|
-
const earlyStopCount =
|
|
390
|
-
.length ?? 0;
|
|
407
|
+
const earlyStopCount = counted.filter((a) => a.early_stop || (a.done_reason && a.done_reason !== "length")).length;
|
|
391
408
|
const checkAttempts = cases
|
|
392
409
|
.filter((c) => c.role === "check")
|
|
393
410
|
.flatMap((c) => c.attempts)
|
|
@@ -404,12 +421,15 @@ function summarizeModel(cases, cold, selfJudge) {
|
|
|
404
421
|
self_judge: selfJudge,
|
|
405
422
|
};
|
|
406
423
|
}
|
|
407
|
-
function printTable(machine, suite, runs, revision, compKey, models, skipped, coldLoad) {
|
|
424
|
+
function printTable(machine, suite, runs, revision, compKey, models, skipped, coldLoad, hot) {
|
|
408
425
|
const lines = [];
|
|
409
|
-
lines.push(`Bench: ${models.length} completion model(s) × suite=${suite} × runs=${runs}`
|
|
426
|
+
lines.push(`Bench: ${models.length} completion model(s) × suite=${suite} × runs=${runs}` +
|
|
427
|
+
(hot ? " (hot: first discarded)" : ""));
|
|
410
428
|
lines.push(`comparability_key=${compKey} suite_revision=${revision} timeout: ${BENCH_TIMEOUT_MS}ms/case`);
|
|
411
429
|
if (coldLoad)
|
|
412
430
|
lines.push("Cold-load: on");
|
|
431
|
+
if (hot)
|
|
432
|
+
lines.push("Hot: first throughput run discarded; models stay loaded");
|
|
413
433
|
lines.push("");
|
|
414
434
|
const header = coldLoad
|
|
415
435
|
? pad("Model", MODEL_COL) +
|
|
@@ -462,6 +482,8 @@ function printTable(machine, suite, runs, revision, compKey, models, skipped, co
|
|
|
462
482
|
notes.push(m.error.slice(0, 60));
|
|
463
483
|
if (m.summary.early_stop_count > 0)
|
|
464
484
|
notes.push(`${m.summary.early_stop_count}× early-stop`);
|
|
485
|
+
if (hot)
|
|
486
|
+
notes.push("hot");
|
|
465
487
|
if (m.summary.self_judge)
|
|
466
488
|
notes.push("self-judge");
|
|
467
489
|
const load = m.summary.load_ms != null && m.summary.load_ms > 0
|
|
@@ -590,11 +612,13 @@ export async function main() {
|
|
|
590
612
|
temperature: 0,
|
|
591
613
|
think: baseSettings.think === true,
|
|
592
614
|
numCtx: baseSettings.num_ctx ?? null,
|
|
615
|
+
hot: parsed.hot,
|
|
593
616
|
});
|
|
594
617
|
const suiteCases = getSuiteCases(parsed.suite);
|
|
618
|
+
const throughputReps = parsed.runs + (parsed.hot ? 1 : 0);
|
|
595
619
|
const caseCount = selected.length *
|
|
596
620
|
(suiteCases.filter((c) => c.role !== "throughput").length +
|
|
597
|
-
suiteCases.filter((c) => c.role === "throughput").length *
|
|
621
|
+
suiteCases.filter((c) => c.role === "throughput").length * throughputReps);
|
|
598
622
|
if (!parsed.json) {
|
|
599
623
|
const skipBits = [
|
|
600
624
|
skipped.filter((s) => s.reason === "non-completion").length
|
|
@@ -606,16 +630,22 @@ export async function main() {
|
|
|
606
630
|
].filter(Boolean);
|
|
607
631
|
console.error(`Bench: ${selected.length} completion model(s)` +
|
|
608
632
|
(skipBits.length ? ` (skipped ${skipBits.join(", ")})` : "") +
|
|
609
|
-
` × suite=${parsed.suite} × runs=${parsed.runs}`
|
|
633
|
+
` × suite=${parsed.suite} × runs=${parsed.runs}` +
|
|
634
|
+
(parsed.hot ? " (hot: +1 discarded)" : ""));
|
|
610
635
|
console.error(`Cases: ~${caseCount} chat calls timeout: ${BENCH_TIMEOUT_MS}ms/case` +
|
|
611
636
|
` worst-case ceiling: ~${Math.ceil((caseCount * BENCH_TIMEOUT_MS) / 60000)}m if every call times out`);
|
|
612
|
-
if (!isLoopback(host)) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
637
|
+
if (!parsed.hot && !isLoopback(host) && (selected.length >= 2 || parsed.coldLoad)) {
|
|
638
|
+
const bits = [
|
|
639
|
+
selected.length >= 2 ? "unloads each model before the next" : "",
|
|
640
|
+
parsed.coldLoad ? "unloads for --cold-load" : "",
|
|
641
|
+
].filter(Boolean);
|
|
642
|
+
console.error(`Note: benchmarking ${bits.join(" and ")} on ${machineLabel}.`);
|
|
616
643
|
}
|
|
617
644
|
console.error("Proceeding…");
|
|
618
645
|
}
|
|
646
|
+
if (parsed.hot) {
|
|
647
|
+
console.error("Hot: discarding the first throughput run; models stay loaded (no unload between models).");
|
|
648
|
+
}
|
|
619
649
|
if (parsed.settings.think === true) {
|
|
620
650
|
for (const name of selected) {
|
|
621
651
|
const caps = await fetchCapabilities(baseUrl, name);
|
|
@@ -665,18 +695,22 @@ export async function main() {
|
|
|
665
695
|
let modelError;
|
|
666
696
|
for (const caseDef of suiteCases) {
|
|
667
697
|
const attempts = [];
|
|
668
|
-
const
|
|
669
|
-
|
|
698
|
+
const discardFirst = parsed.hot && caseDef.role === "throughput";
|
|
699
|
+
const reps = caseDef.role === "throughput" ? parsed.runs + (discardFirst ? 1 : 0) : 1;
|
|
700
|
+
for (let i = 0; i < reps; i += 1) {
|
|
701
|
+
const discarded = discardFirst && i === 0;
|
|
670
702
|
const attempt = await runCaseAttempt({
|
|
671
703
|
baseUrl,
|
|
672
704
|
model,
|
|
673
705
|
caseDef,
|
|
674
|
-
run:
|
|
706
|
+
run: discarded ? 0 : discardFirst ? i : i + 1,
|
|
675
707
|
settings: baseSettings,
|
|
676
708
|
throughputNumPredict: parsed.throughputNumPredict,
|
|
677
709
|
judgeModel,
|
|
678
710
|
selfJudge,
|
|
679
711
|
});
|
|
712
|
+
if (discarded)
|
|
713
|
+
attempt.discarded = true;
|
|
680
714
|
attempts.push(attempt);
|
|
681
715
|
if (attempt.error) {
|
|
682
716
|
anyError = true;
|
|
@@ -717,8 +751,9 @@ export async function main() {
|
|
|
717
751
|
});
|
|
718
752
|
if (parsed.failFast && modelError)
|
|
719
753
|
break;
|
|
720
|
-
// Unload before next model when ≥2 models remain after this one / multi-model run
|
|
721
|
-
|
|
754
|
+
// Unload before next model when ≥2 models remain after this one / multi-model run.
|
|
755
|
+
// --hot keeps weights resident so the discarded first shot warms the rest.
|
|
756
|
+
if (!parsed.hot && selected.length >= 2 && mi < selected.length - 1) {
|
|
722
757
|
try {
|
|
723
758
|
await ollamaUnload(baseUrl, model, BENCH_TIMEOUT_MS);
|
|
724
759
|
await waitUntilUnloaded(baseUrl, model, UNLOAD_WAIT_MS);
|
|
@@ -755,6 +790,7 @@ export async function main() {
|
|
|
755
790
|
throughput_num_predict: parsed.throughputNumPredict,
|
|
756
791
|
num_ctx: baseSettings.num_ctx ?? null,
|
|
757
792
|
warmup: parsed.warmup,
|
|
793
|
+
hot: parsed.hot,
|
|
758
794
|
cold_load: parsed.coldLoad,
|
|
759
795
|
think: baseSettings.think === true,
|
|
760
796
|
bench_timeout_ms: BENCH_TIMEOUT_MS,
|
|
@@ -778,7 +814,7 @@ export async function main() {
|
|
|
778
814
|
}
|
|
779
815
|
else {
|
|
780
816
|
process.stdout.write("\n");
|
|
781
|
-
printTable(machineLabel, parsed.suite, parsed.runs, revision, compKey, modelResults, skipped, parsed.coldLoad);
|
|
817
|
+
printTable(machineLabel, parsed.suite, parsed.runs, revision, compKey, modelResults, skipped, parsed.coldLoad, parsed.hot);
|
|
782
818
|
if (parsed.save)
|
|
783
819
|
console.error(`benchmarks dir: ${benchmarksDir()}`);
|
|
784
820
|
}
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* ollanet scan
|
|
6
6
|
* ollanet prompt <machine-or-ip> "hello"
|
|
7
|
+
* ollanet pull <machine> <model>
|
|
8
|
+
* ollanet show <machine> <model>
|
|
9
|
+
* ollanet rm <machine> <model> --yes
|
|
10
|
+
* ollanet ps [machine]
|
|
7
11
|
* ollanet chats
|
|
8
12
|
*/
|
|
9
13
|
const HELP = `ollanet — Ollama over your network
|
|
@@ -12,9 +16,15 @@ Discover Ollama hosts on localhost, your LAN, Tailscale, or any IP/hostname
|
|
|
12
16
|
you can reach — then chat with hash-addressed transcripts.
|
|
13
17
|
|
|
14
18
|
Usage:
|
|
15
|
-
ollanet scan [--json] [--all] [--lan]
|
|
19
|
+
ollanet scan [--json] [--all] [--lan] [--last]
|
|
16
20
|
ollanet prompt <machine|ip> [model] <prompt...>
|
|
17
21
|
ollanet prompt --chat <hash> <prompt...>
|
|
22
|
+
ollanet prompt <machine|ip> --file <path.txt|.md>
|
|
23
|
+
ollanet compare <machine> <model> <model> [model...] [--prompt <text> | --file <path>]
|
|
24
|
+
ollanet pull <machine|ip> <model>
|
|
25
|
+
ollanet show <machine|ip> <model>
|
|
26
|
+
ollanet rm <machine|ip> <model> --yes
|
|
27
|
+
ollanet ps [machine]
|
|
18
28
|
ollanet chats [--json] [--id <hash>]
|
|
19
29
|
ollanet bench <machine|ip> [model...] [--all] [options]
|
|
20
30
|
ollanet mcp
|
|
@@ -22,18 +32,27 @@ Usage:
|
|
|
22
32
|
Examples:
|
|
23
33
|
ollanet scan
|
|
24
34
|
ollanet scan --lan
|
|
35
|
+
ollanet scan --last # print the last saved scan (no network)
|
|
25
36
|
ollanet prompt localhost "What is MagicDNS?"
|
|
26
|
-
ollanet prompt
|
|
37
|
+
ollanet prompt localhost --file ./notes.md
|
|
38
|
+
ollanet compare studio gemma3:12b llama3.2:3b
|
|
39
|
+
ollanet compare studio gemma3:12b llama3.2:3b --prompt "Explain MagicDNS"
|
|
27
40
|
ollanet prompt --chat a1b2c3d4e5f6 "Tell me more"
|
|
41
|
+
ollanet pull studio gemma3:12b
|
|
42
|
+
ollanet show studio gemma4-ctx32k
|
|
43
|
+
ollanet rm studio llama3.2:1b --yes
|
|
44
|
+
ollanet ps studio
|
|
28
45
|
ollanet chats
|
|
29
46
|
ollanet bench localhost --all
|
|
30
47
|
ollanet bench localhost llama3.2:1b --runs 3
|
|
48
|
+
ollanet bench localhost gemma3:12b --hot --runs 5
|
|
31
49
|
ollanet mcp # stdio MCP server for agents
|
|
32
50
|
|
|
33
51
|
Config: ~/.ollanet/config.json when installed, ./config.json in a checkout
|
|
34
52
|
(override with OLLANET_CONFIG)
|
|
35
53
|
Hosts: config.hosts, OLLANET_HOSTS, optional Tailscale + --lan
|
|
36
54
|
Chats: ~/.ollanet/responses or ./responses (override with OLLANET_RESPONSES_DIR)
|
|
55
|
+
Scan: ~/.ollanet/last-scan.json or ./last-scan.json (override with OLLANET_LAST_SCAN)
|
|
37
56
|
`;
|
|
38
57
|
async function main() {
|
|
39
58
|
// pnpm/npm often forward a bare `--`; ignore it.
|
|
@@ -73,6 +92,37 @@ async function main() {
|
|
|
73
92
|
await run();
|
|
74
93
|
return;
|
|
75
94
|
}
|
|
95
|
+
case "pull":
|
|
96
|
+
case "download": {
|
|
97
|
+
const { main: run } = await import("./pull.js");
|
|
98
|
+
await run();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
case "show":
|
|
102
|
+
case "inspect": {
|
|
103
|
+
const { main: run } = await import("./show.js");
|
|
104
|
+
await run();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
case "rm":
|
|
108
|
+
case "delete": {
|
|
109
|
+
const { main: run } = await import("./rm.js");
|
|
110
|
+
await run();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
case "ps":
|
|
114
|
+
case "loaded": {
|
|
115
|
+
const { main: run } = await import("./ps.js");
|
|
116
|
+
await run();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
case "compare":
|
|
120
|
+
case "vs":
|
|
121
|
+
case "h2h": {
|
|
122
|
+
const { main: run } = await import("./compare.js");
|
|
123
|
+
await run();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
76
126
|
case "bench":
|
|
77
127
|
case "benchmark": {
|
|
78
128
|
const { main: run } = await import("./bench.js");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CompareRecord } from "./compare.js";
|
|
2
|
+
export declare function comparesDir(): string;
|
|
3
|
+
export declare function newCompareId(): string;
|
|
4
|
+
export declare function renderCompareMarkdown(record: CompareRecord): string;
|
|
5
|
+
export declare function saveCompare(record: CompareRecord): Promise<{
|
|
6
|
+
json: string;
|
|
7
|
+
md: string;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { defaultComparesDir } from "./paths.js";
|
|
5
|
+
export function comparesDir() {
|
|
6
|
+
return process.env.OLLANET_COMPARES_DIR ?? defaultComparesDir();
|
|
7
|
+
}
|
|
8
|
+
export function newCompareId() {
|
|
9
|
+
return randomBytes(6).toString("hex");
|
|
10
|
+
}
|
|
11
|
+
function tokLabel(n) {
|
|
12
|
+
return n == null || !Number.isFinite(n) ? "—" : n.toFixed(1);
|
|
13
|
+
}
|
|
14
|
+
export function renderCompareMarkdown(record) {
|
|
15
|
+
const lines = [
|
|
16
|
+
`# Compare ${record.id}`,
|
|
17
|
+
"",
|
|
18
|
+
`- **host:** ${record.machine} (${record.endpoint})`,
|
|
19
|
+
`- **when:** ${record.created_at}`,
|
|
20
|
+
`- **models:** ${record.results.map((r) => r.model).join(", ")}`,
|
|
21
|
+
...(record.default_prompt
|
|
22
|
+
? ["- **prompt:** default (override with --prompt / --file)"]
|
|
23
|
+
: []),
|
|
24
|
+
"",
|
|
25
|
+
"## Prompt",
|
|
26
|
+
"",
|
|
27
|
+
record.prompt.trimEnd(),
|
|
28
|
+
"",
|
|
29
|
+
];
|
|
30
|
+
if (record.system) {
|
|
31
|
+
lines.push("## System", "", record.system.trimEnd(), "");
|
|
32
|
+
}
|
|
33
|
+
for (const r of record.results) {
|
|
34
|
+
lines.push(`## ${r.model}`, "");
|
|
35
|
+
if (r.error) {
|
|
36
|
+
lines.push(`- **error:** ${r.error}`, "");
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
lines.push(`- **tok/s:** ${tokLabel(r.tok_s)}`, `- **tokens:** ${r.eval_count ?? "—"}`, `- **wall:** ${r.wall_ms != null ? `${(r.wall_ms / 1000).toFixed(2)}s` : "—"}`, `- **done:** ${r.done_reason ?? "—"}`, "");
|
|
40
|
+
if (r.thinking) {
|
|
41
|
+
lines.push("### Thinking", "", r.thinking.trimEnd(), "");
|
|
42
|
+
}
|
|
43
|
+
lines.push(r.content?.trimEnd() || "_(empty reply)_", "");
|
|
44
|
+
}
|
|
45
|
+
return `${lines.join("\n")}\n`;
|
|
46
|
+
}
|
|
47
|
+
export async function saveCompare(record) {
|
|
48
|
+
const dir = comparesDir();
|
|
49
|
+
await mkdir(dir, { recursive: true });
|
|
50
|
+
const json = path.join(dir, `${record.id}.json`);
|
|
51
|
+
const md = path.join(dir, `${record.id}.md`);
|
|
52
|
+
await writeFile(json, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
53
|
+
await writeFile(md, renderCompareMarkdown(record), "utf8");
|
|
54
|
+
return { json, md };
|
|
55
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Head-to-head: one prompt, 2–5 models on the same host.
|
|
3
|
+
*
|
|
4
|
+
* ollanet compare studio gemma3:12b llama3.2:3b --prompt "Explain MagicDNS"
|
|
5
|
+
* ollanet compare studio m1 m2 --file task.md
|
|
6
|
+
*/
|
|
7
|
+
import { type AppConfig, type GenerateSettings } from "./config.js";
|
|
8
|
+
/** Used when --prompt, --file, and stdin are all omitted. */
|
|
9
|
+
export declare const DEFAULT_COMPARE_PROMPT: string;
|
|
10
|
+
export interface CompareModelResult {
|
|
11
|
+
model: string;
|
|
12
|
+
content?: string;
|
|
13
|
+
thinking?: string;
|
|
14
|
+
tok_s?: number;
|
|
15
|
+
eval_count?: number;
|
|
16
|
+
eval_duration?: number;
|
|
17
|
+
total_duration?: number;
|
|
18
|
+
load_duration?: number;
|
|
19
|
+
wall_ms?: number;
|
|
20
|
+
done_reason?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface CompareRecord {
|
|
24
|
+
id: string;
|
|
25
|
+
created_at: string;
|
|
26
|
+
machine: string;
|
|
27
|
+
endpoint: string;
|
|
28
|
+
prompt: string;
|
|
29
|
+
/** True when the built-in default prompt was used. */
|
|
30
|
+
default_prompt: boolean;
|
|
31
|
+
system?: string;
|
|
32
|
+
results: CompareModelResult[];
|
|
33
|
+
files?: {
|
|
34
|
+
json: string;
|
|
35
|
+
md: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface CompareOptions {
|
|
39
|
+
machine: string;
|
|
40
|
+
models: string[];
|
|
41
|
+
prompt?: string;
|
|
42
|
+
file?: string;
|
|
43
|
+
settings?: GenerateSettings;
|
|
44
|
+
save?: boolean;
|
|
45
|
+
unload?: boolean;
|
|
46
|
+
writeStdout?: boolean;
|
|
47
|
+
quiet?: boolean;
|
|
48
|
+
config?: Partial<AppConfig>;
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
}
|
|
51
|
+
export declare function runCompare(opts: CompareOptions): Promise<CompareRecord>;
|
|
52
|
+
export declare function main(): Promise<void>;
|