ollanet 0.5.0 → 0.6.1
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 +41 -7
- package/dist/argv.d.ts +17 -0
- package/dist/argv.js +73 -0
- package/dist/bench-suite.d.ts +2 -0
- package/dist/bench-suite.js +1 -0
- package/dist/bench.js +75 -79
- package/dist/chats.js +4 -6
- package/dist/cli.js +16 -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 +288 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +4 -0
- package/dist/mcp.js +118 -3
- 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 +46 -72
- package/dist/ps.js +4 -10
- package/dist/pull.js +7 -12
- package/dist/rm.js +7 -12
- package/dist/scan-store.d.ts +15 -0
- package/dist/scan-store.js +41 -0
- package/dist/scan.d.ts +12 -1
- package/dist/scan.js +73 -42
- package/dist/show.js +7 -12
- package/dist/target.d.ts +2 -0
- package/dist/target.js +10 -5
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -32,11 +32,12 @@ They share the same Ollama API; Finetuna shapes the models, ollanet finds and ch
|
|
|
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
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
|
|
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`
|
|
36
37
|
- **Persist** chats as `responses/<hash>.json` with topic, machine, model, timestamps
|
|
37
38
|
- **Continue** any thread with `--chat <hash>`
|
|
38
39
|
- **Configure** per-machine defaults (model, temperature, context, …)
|
|
39
|
-
- **MCP** — `ollanet mcp` exposes scan/prompt/pull/show/rm/ps/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
|
|
40
41
|
- **Library** — `import { scanNetwork } from "ollanet"` for apps (Node 20+, not the browser)
|
|
41
42
|
|
|
42
43
|
## Use cases
|
|
@@ -125,6 +126,8 @@ ollanet ps studio
|
|
|
125
126
|
|
|
126
127
|
# Talk to a machine by name, MagicDNS name, or IP
|
|
127
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"
|
|
128
131
|
ollanet prompt 192.168.1.50 gemma4:12b "Hello from the LAN"
|
|
129
132
|
|
|
130
133
|
# Continue later
|
|
@@ -154,6 +157,7 @@ for (const s of servers) {
|
|
|
154
157
|
|---|---|---|
|
|
155
158
|
| `scanNetwork()` | localhost + config + `OLLANET_HOSTS` + Tailscale | Button / first “find servers” |
|
|
156
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” |
|
|
157
161
|
| `discoverHosts()` then probe yourself | candidates only, no `/api/tags` | If the app already has its own Ollama client |
|
|
158
162
|
|
|
159
163
|
Pass `config` to skip the file (useful when the app already has its own config):
|
|
@@ -172,7 +176,7 @@ await scanNetwork({
|
|
|
172
176
|
|
|
173
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.
|
|
174
178
|
|
|
175
|
-
Requires **ollanet ≥ 0.4.0** (library). **Model management (`pull` / `show` / `rm` / `ps`) needs ≥ 0.5.0.** Bundlers (Vite / SvelteKit SSR):
|
|
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.** **`resolveTarget` / `listTargets` need ≥ 0.6.1.** Bundlers (Vite / SvelteKit SSR):
|
|
176
180
|
|
|
177
181
|
```ts
|
|
178
182
|
// vite.config.ts
|
|
@@ -189,7 +193,8 @@ optimizeDeps: { exclude: ["ollanet"] },
|
|
|
189
193
|
| `ollanet show <machine> <model>` | Inspect Modelfile / params / capabilities (`[tuned]` when it looks like Finetuna) |
|
|
190
194
|
| `ollanet rm <machine> <model> --yes` | Delete a model from that host’s disk |
|
|
191
195
|
| `ollanet ps [machine]` | Models loaded in VRAM (omit machine = every discovered host) |
|
|
192
|
-
| `ollanet prompt …` | Send a prompt / continue a chat |
|
|
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` |
|
|
193
198
|
| `ollanet chats` | List or inspect saved transcripts |
|
|
194
199
|
| `ollanet bench …` | Benchmark models for tok/s + lightweight quality checks |
|
|
195
200
|
| `ollanet mcp` | Stdio [MCP](https://modelcontextprotocol.io) server for agents |
|
|
@@ -199,6 +204,7 @@ optimizeDeps: { exclude: ["ollanet"] },
|
|
|
199
204
|
- `--json` — machine-readable output
|
|
200
205
|
- `--all` — also probe offline Tailscale peers
|
|
201
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).
|
|
202
208
|
|
|
203
209
|
### `prompt` options
|
|
204
210
|
|
|
@@ -219,12 +225,36 @@ ollanet prompt --chat <hash> <prompt...>
|
|
|
219
225
|
| `--keep-alive <value>` | Keep model loaded (`5m`, `0`, `-1`, …) |
|
|
220
226
|
| `--format json\|<schema>` | JSON mode / schema |
|
|
221
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) |
|
|
222
229
|
| `--no-stream` | Buffer the full reply |
|
|
223
230
|
| `--no-save` | Do not write a transcript |
|
|
224
231
|
| `--json` | Emit JSON (includes chat id when saved) |
|
|
225
232
|
|
|
226
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.
|
|
227
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
|
+
|
|
228
258
|
### `pull` options
|
|
229
259
|
|
|
230
260
|
```text
|
|
@@ -274,8 +304,9 @@ Runs a **stdio MCP server** (no extra npm deps). Cursor / Claude Desktop / any M
|
|
|
274
304
|
|
|
275
305
|
| Tool | What it does |
|
|
276
306
|
|---|---|
|
|
277
|
-
| `ollanet_scan` | Discover hosts + models (`lan`, `all` optional) |
|
|
278
|
-
| `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) |
|
|
279
310
|
| `ollanet_pull` | Pull / update a model on a host (`machine`, `model`) |
|
|
280
311
|
| `ollanet_show` | Inspect a model (`machine`, `model`) |
|
|
281
312
|
| `ollanet_rm` | Delete a model (`machine`, `model`, `confirm: true`) |
|
|
@@ -304,11 +335,14 @@ Or, from a checkout: `"command": "pnpm"`, `"args": ["ollanet", "--", "mcp"]` (wi
|
|
|
304
335
|
ollanet bench <machine|ip> [model...]
|
|
305
336
|
ollanet bench <machine|ip> --all
|
|
306
337
|
ollanet bench localhost gemma4:12b --runs 5 --cold-load --json
|
|
338
|
+
ollanet bench localhost gemma4:12b --hot --runs 5
|
|
307
339
|
```
|
|
308
340
|
|
|
309
341
|
Runs a built-in suite (`quick` default, or `--suite full`) against one or more **completion**
|
|
310
342
|
models: instruction checks once, throughput case repeated (`--runs`, default 3) with
|
|
311
|
-
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`,
|
|
312
346
|
skips non-completion models (embeddings); multimodal `completion`+`vision` models stay in
|
|
313
347
|
(early-stop / pass_rate handle text-suite failures). Use `--exclude-vision` to drop them.
|
|
314
348
|
Saves `benchmarks/<id>.json` (or `~/.ollanet/benchmarks/` when installed).
|
package/dist/argv.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI flag helpers. Commands keep their own usage() and unique flags.
|
|
3
|
+
*/
|
|
4
|
+
import type { GenerateSettings } from "./config.js";
|
|
5
|
+
export type UsageFn = () => never;
|
|
6
|
+
export declare function takeValue(args: string[], flag: string, usage: UsageFn): string;
|
|
7
|
+
/** `--name value` or `--name=value`. Returns undefined if `arg` is not this flag. */
|
|
8
|
+
export declare function takeFlag(arg: string, name: string, args: string[], usage: UsageFn): string | undefined;
|
|
9
|
+
export declare function parseNumberFlag(raw: string, flag: string, usage: UsageFn): number;
|
|
10
|
+
export declare function parseKeepAlive(raw: string): string | number;
|
|
11
|
+
export interface SettingsFlagOpts {
|
|
12
|
+
temperature?: boolean;
|
|
13
|
+
numPredict?: boolean;
|
|
14
|
+
system?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Shared generate-settings flags. Returns true when `arg` was consumed. */
|
|
17
|
+
export declare function consumeSettingsFlag(arg: string, args: string[], settings: GenerateSettings, usage: UsageFn, opts?: SettingsFlagOpts): boolean;
|
package/dist/argv.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function takeValue(args, flag, usage) {
|
|
2
|
+
const value = args.shift();
|
|
3
|
+
if (!value) {
|
|
4
|
+
console.error(`${flag} requires a value`);
|
|
5
|
+
usage();
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
/** `--name value` or `--name=value`. Returns undefined if `arg` is not this flag. */
|
|
10
|
+
export function takeFlag(arg, name, args, usage) {
|
|
11
|
+
if (arg === name)
|
|
12
|
+
return takeValue(args, name, usage);
|
|
13
|
+
if (arg.startsWith(`${name}=`))
|
|
14
|
+
return arg.slice(name.length + 1);
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
export function parseNumberFlag(raw, flag, usage) {
|
|
18
|
+
const n = Number(raw);
|
|
19
|
+
if (!Number.isFinite(n)) {
|
|
20
|
+
console.error(`${flag} must be a number (got "${raw}")`);
|
|
21
|
+
usage();
|
|
22
|
+
}
|
|
23
|
+
return n;
|
|
24
|
+
}
|
|
25
|
+
export function parseKeepAlive(raw) {
|
|
26
|
+
const asNum = Number(raw);
|
|
27
|
+
return raw.trim() !== "" && Number.isFinite(asNum) && String(asNum) === raw ? asNum : raw;
|
|
28
|
+
}
|
|
29
|
+
/** Shared generate-settings flags. Returns true when `arg` was consumed. */
|
|
30
|
+
export function consumeSettingsFlag(arg, args, settings, usage, opts = {}) {
|
|
31
|
+
if (arg === "--think") {
|
|
32
|
+
settings.think = true;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (arg === "--no-think") {
|
|
36
|
+
settings.think = false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (opts.system) {
|
|
40
|
+
const system = takeFlag(arg, "--system", args, usage);
|
|
41
|
+
if (system !== undefined) {
|
|
42
|
+
settings.system = system;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (opts.temperature) {
|
|
47
|
+
if (arg === "-t" || arg === "--temperature" || arg.startsWith("--temperature=")) {
|
|
48
|
+
const raw = arg === "-t" || arg === "--temperature"
|
|
49
|
+
? takeValue(args, "--temperature", usage)
|
|
50
|
+
: arg.slice("--temperature=".length);
|
|
51
|
+
settings.temperature = parseNumberFlag(raw, "--temperature", usage);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (opts.numPredict) {
|
|
56
|
+
const raw = takeFlag(arg, "--num-predict", args, usage);
|
|
57
|
+
if (raw !== undefined) {
|
|
58
|
+
settings.num_predict = Math.trunc(parseNumberFlag(raw, "--num-predict", usage));
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const numCtx = takeFlag(arg, "--num-ctx", args, usage);
|
|
63
|
+
if (numCtx !== undefined) {
|
|
64
|
+
settings.num_ctx = Math.trunc(parseNumberFlag(numCtx, "--num-ctx", usage));
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const keepAlive = takeFlag(arg, "--keep-alive", args, usage);
|
|
68
|
+
if (keepAlive !== undefined) {
|
|
69
|
+
settings.keep_alive = parseKeepAlive(keepAlive);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
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
|
@@ -5,7 +5,9 @@ import { defaultModelForHost, loadConfig, machineSettingsForHost, mergeSettings,
|
|
|
5
5
|
import { comparabilityKey, getSuiteCases, median, suiteRevision, } from "./bench-suite.js";
|
|
6
6
|
import { benchmarksDir, newBenchId, saveBenchmark } from "./bench-store.js";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
|
-
import {
|
|
8
|
+
import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
|
|
9
|
+
import { consumeSettingsFlag, parseNumberFlag, takeFlag } from "./argv.js";
|
|
10
|
+
import { resolveTarget } from "./target.js";
|
|
9
11
|
import { projectPath } from "./paths.js";
|
|
10
12
|
import { contextLengthForModel, isCompletionCapable, isVisionCapable, ollamaChat, ollamaShow, ollamaTags, ollamaUnload, ollamaVersion, ollamaPs, psHasModel, shouldWarnNoThinking, vramForModel, waitUntilUnloaded, } from "./ollama-chat.js";
|
|
11
13
|
const MODEL_COL = 34;
|
|
@@ -16,11 +18,16 @@ function usage() {
|
|
|
16
18
|
ollanet bench <machine|ip> [model...] [options]
|
|
17
19
|
ollanet bench <machine|ip> --all [options]
|
|
18
20
|
|
|
21
|
+
Examples:
|
|
22
|
+
ollanet bench studio gemma3:12b --runs 5
|
|
23
|
+
ollanet bench studio gemma3:12b --hot --runs 5
|
|
24
|
+
|
|
19
25
|
Options:
|
|
20
26
|
--all Every completion-capable model from /api/tags
|
|
21
27
|
--exclude-vision With --all, skip models that advertise vision
|
|
22
28
|
--suite quick|full Prompt suite (default quick)
|
|
23
29
|
--runs <n> Throughput repeats (default 3)
|
|
30
|
+
--hot Discard first throughput run; keep models loaded
|
|
24
31
|
--warmup / --no-warmup
|
|
25
32
|
--cold-load Measure cold load via unload + /api/ps
|
|
26
33
|
--num-predict <n> Pin throughput length (default 256)
|
|
@@ -36,22 +43,6 @@ Options:
|
|
|
36
43
|
See docs/bench-spec.md for measurement rules.`);
|
|
37
44
|
process.exit(1);
|
|
38
45
|
}
|
|
39
|
-
function takeValue(args, flag) {
|
|
40
|
-
const value = args.shift();
|
|
41
|
-
if (!value) {
|
|
42
|
-
console.error(`${flag} requires a value`);
|
|
43
|
-
usage();
|
|
44
|
-
}
|
|
45
|
-
return value;
|
|
46
|
-
}
|
|
47
|
-
function parseNumberFlag(raw, flag) {
|
|
48
|
-
const n = Number(raw);
|
|
49
|
-
if (!Number.isFinite(n)) {
|
|
50
|
-
console.error(`${flag} must be a number (got "${raw}")`);
|
|
51
|
-
usage();
|
|
52
|
-
}
|
|
53
|
-
return n;
|
|
54
|
-
}
|
|
55
46
|
function isLoopback(host) {
|
|
56
47
|
return (host.source === "localhost" ||
|
|
57
48
|
host.ip === "127.0.0.1" ||
|
|
@@ -85,6 +76,8 @@ function parseArgs(argv) {
|
|
|
85
76
|
let all = false;
|
|
86
77
|
let excludeVision = false;
|
|
87
78
|
let warmup = true;
|
|
79
|
+
let warmupSet = false;
|
|
80
|
+
let hot = false;
|
|
88
81
|
let coldLoad = false;
|
|
89
82
|
let json = false;
|
|
90
83
|
let save = true;
|
|
@@ -126,10 +119,16 @@ function parseArgs(argv) {
|
|
|
126
119
|
}
|
|
127
120
|
if (arg === "--warmup") {
|
|
128
121
|
warmup = true;
|
|
122
|
+
warmupSet = true;
|
|
129
123
|
continue;
|
|
130
124
|
}
|
|
131
125
|
if (arg === "--no-warmup") {
|
|
132
126
|
warmup = false;
|
|
127
|
+
warmupSet = true;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (arg === "--hot") {
|
|
131
|
+
hot = true;
|
|
133
132
|
continue;
|
|
134
133
|
}
|
|
135
134
|
if (arg === "--cold-load") {
|
|
@@ -144,57 +143,35 @@ function parseArgs(argv) {
|
|
|
144
143
|
failOnError = true;
|
|
145
144
|
continue;
|
|
146
145
|
}
|
|
147
|
-
if (arg === "--think") {
|
|
148
|
-
settings.think = true;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (arg === "--no-think") {
|
|
152
|
-
settings.think = false;
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
146
|
if (arg === "--judge") {
|
|
156
147
|
judge = true;
|
|
157
148
|
continue;
|
|
158
149
|
}
|
|
159
|
-
if (arg
|
|
160
|
-
const raw = arg.includes("=") ? arg.slice("--suite=".length) : takeValue(args, "--suite");
|
|
161
|
-
if (raw !== "quick" && raw !== "full") {
|
|
162
|
-
console.error(`Unknown suite "${raw}" (use quick|full)`);
|
|
163
|
-
usage();
|
|
164
|
-
}
|
|
165
|
-
suite = raw;
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (arg === "--runs" || arg.startsWith("--runs=")) {
|
|
169
|
-
const raw = arg.includes("=") ? arg.slice("--runs=".length) : takeValue(args, "--runs");
|
|
170
|
-
runs = Math.max(1, Math.trunc(parseNumberFlag(raw, "--runs")));
|
|
150
|
+
if (consumeSettingsFlag(arg, args, settings, usage)) {
|
|
171
151
|
continue;
|
|
172
152
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
153
|
+
const suiteRaw = takeFlag(arg, "--suite", args, usage);
|
|
154
|
+
if (suiteRaw !== undefined) {
|
|
155
|
+
if (suiteRaw !== "quick" && suiteRaw !== "full") {
|
|
156
|
+
console.error(`Unknown suite "${suiteRaw}" (use quick|full)`);
|
|
157
|
+
usage();
|
|
158
|
+
}
|
|
159
|
+
suite = suiteRaw;
|
|
178
160
|
continue;
|
|
179
161
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
162
|
+
const runsRaw = takeFlag(arg, "--runs", args, usage);
|
|
163
|
+
if (runsRaw !== undefined) {
|
|
164
|
+
runs = Math.max(1, Math.trunc(parseNumberFlag(runsRaw, "--runs", usage)));
|
|
183
165
|
continue;
|
|
184
166
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
: takeValue(args, "--keep-alive");
|
|
189
|
-
const asNum = Number(raw);
|
|
190
|
-
settings.keep_alive =
|
|
191
|
-
raw.trim() !== "" && Number.isFinite(asNum) && String(asNum) === raw ? asNum : raw;
|
|
167
|
+
const predictRaw = takeFlag(arg, "--num-predict", args, usage);
|
|
168
|
+
if (predictRaw !== undefined) {
|
|
169
|
+
throughputNumPredict = Math.trunc(parseNumberFlag(predictRaw, "--num-predict", usage));
|
|
192
170
|
continue;
|
|
193
171
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
: takeValue(args, "--judge-model");
|
|
172
|
+
const judgeRaw = takeFlag(arg, "--judge-model", args, usage);
|
|
173
|
+
if (judgeRaw !== undefined) {
|
|
174
|
+
judgeModel = judgeRaw;
|
|
198
175
|
continue;
|
|
199
176
|
}
|
|
200
177
|
if (arg.startsWith("-")) {
|
|
@@ -221,7 +198,8 @@ function parseArgs(argv) {
|
|
|
221
198
|
excludeVision,
|
|
222
199
|
suite,
|
|
223
200
|
runs,
|
|
224
|
-
warmup,
|
|
201
|
+
warmup: hot && !warmupSet ? false : warmup,
|
|
202
|
+
hot,
|
|
225
203
|
coldLoad,
|
|
226
204
|
json,
|
|
227
205
|
save,
|
|
@@ -382,12 +360,15 @@ async function runCaseAttempt(opts) {
|
|
|
382
360
|
};
|
|
383
361
|
}
|
|
384
362
|
}
|
|
363
|
+
function countedAttempts(attempts) {
|
|
364
|
+
return attempts.filter((a) => !a.discarded);
|
|
365
|
+
}
|
|
385
366
|
function summarizeModel(cases, cold, selfJudge) {
|
|
386
367
|
const throughput = cases.find((c) => c.id === "throughput");
|
|
387
|
-
const
|
|
368
|
+
const counted = countedAttempts(throughput?.attempts ?? []);
|
|
369
|
+
const fullLen = counted.filter((a) => !a.error && !a.early_stop && a.tok_s != null);
|
|
388
370
|
const toks = fullLen.map((a) => a.tok_s);
|
|
389
|
-
const earlyStopCount =
|
|
390
|
-
.length ?? 0;
|
|
371
|
+
const earlyStopCount = counted.filter((a) => a.early_stop || (a.done_reason && a.done_reason !== "length")).length;
|
|
391
372
|
const checkAttempts = cases
|
|
392
373
|
.filter((c) => c.role === "check")
|
|
393
374
|
.flatMap((c) => c.attempts)
|
|
@@ -404,12 +385,15 @@ function summarizeModel(cases, cold, selfJudge) {
|
|
|
404
385
|
self_judge: selfJudge,
|
|
405
386
|
};
|
|
406
387
|
}
|
|
407
|
-
function printTable(machine, suite, runs, revision, compKey, models, skipped, coldLoad) {
|
|
388
|
+
function printTable(machine, suite, runs, revision, compKey, models, skipped, coldLoad, hot) {
|
|
408
389
|
const lines = [];
|
|
409
|
-
lines.push(`Bench: ${models.length} completion model(s) × suite=${suite} × runs=${runs}`
|
|
390
|
+
lines.push(`Bench: ${models.length} completion model(s) × suite=${suite} × runs=${runs}` +
|
|
391
|
+
(hot ? " (hot: first discarded)" : ""));
|
|
410
392
|
lines.push(`comparability_key=${compKey} suite_revision=${revision} timeout: ${BENCH_TIMEOUT_MS}ms/case`);
|
|
411
393
|
if (coldLoad)
|
|
412
394
|
lines.push("Cold-load: on");
|
|
395
|
+
if (hot)
|
|
396
|
+
lines.push("Hot: first throughput run discarded; models stay loaded");
|
|
413
397
|
lines.push("");
|
|
414
398
|
const header = coldLoad
|
|
415
399
|
? pad("Model", MODEL_COL) +
|
|
@@ -462,6 +446,8 @@ function printTable(machine, suite, runs, revision, compKey, models, skipped, co
|
|
|
462
446
|
notes.push(m.error.slice(0, 60));
|
|
463
447
|
if (m.summary.early_stop_count > 0)
|
|
464
448
|
notes.push(`${m.summary.early_stop_count}× early-stop`);
|
|
449
|
+
if (hot)
|
|
450
|
+
notes.push("hot");
|
|
465
451
|
if (m.summary.self_judge)
|
|
466
452
|
notes.push("self-judge");
|
|
467
453
|
const load = m.summary.load_ms != null && m.summary.load_ms > 0
|
|
@@ -505,11 +491,7 @@ function pad(s, n) {
|
|
|
505
491
|
export async function main() {
|
|
506
492
|
const parsed = parseArgs(process.argv.slice(2));
|
|
507
493
|
const config = await loadConfig();
|
|
508
|
-
const
|
|
509
|
-
hosts: config.hosts,
|
|
510
|
-
discovery: config.discovery,
|
|
511
|
-
});
|
|
512
|
-
const host = resolveHost(targets, parsed.machine);
|
|
494
|
+
const host = await resolveTarget(parsed.machine, config);
|
|
513
495
|
const baseUrl = ollamaBaseUrl(host);
|
|
514
496
|
const machineLabel = shortName(host);
|
|
515
497
|
const tags = await ollamaTags(baseUrl, Math.min(BENCH_TIMEOUT_MS, 15_000));
|
|
@@ -590,11 +572,13 @@ export async function main() {
|
|
|
590
572
|
temperature: 0,
|
|
591
573
|
think: baseSettings.think === true,
|
|
592
574
|
numCtx: baseSettings.num_ctx ?? null,
|
|
575
|
+
hot: parsed.hot,
|
|
593
576
|
});
|
|
594
577
|
const suiteCases = getSuiteCases(parsed.suite);
|
|
578
|
+
const throughputReps = parsed.runs + (parsed.hot ? 1 : 0);
|
|
595
579
|
const caseCount = selected.length *
|
|
596
580
|
(suiteCases.filter((c) => c.role !== "throughput").length +
|
|
597
|
-
suiteCases.filter((c) => c.role === "throughput").length *
|
|
581
|
+
suiteCases.filter((c) => c.role === "throughput").length * throughputReps);
|
|
598
582
|
if (!parsed.json) {
|
|
599
583
|
const skipBits = [
|
|
600
584
|
skipped.filter((s) => s.reason === "non-completion").length
|
|
@@ -606,16 +590,22 @@ export async function main() {
|
|
|
606
590
|
].filter(Boolean);
|
|
607
591
|
console.error(`Bench: ${selected.length} completion model(s)` +
|
|
608
592
|
(skipBits.length ? ` (skipped ${skipBits.join(", ")})` : "") +
|
|
609
|
-
` × suite=${parsed.suite} × runs=${parsed.runs}`
|
|
593
|
+
` × suite=${parsed.suite} × runs=${parsed.runs}` +
|
|
594
|
+
(parsed.hot ? " (hot: +1 discarded)" : ""));
|
|
610
595
|
console.error(`Cases: ~${caseCount} chat calls timeout: ${BENCH_TIMEOUT_MS}ms/case` +
|
|
611
596
|
` worst-case ceiling: ~${Math.ceil((caseCount * BENCH_TIMEOUT_MS) / 60000)}m if every call times out`);
|
|
612
|
-
if (!isLoopback(host)) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
597
|
+
if (!parsed.hot && !isLoopback(host) && (selected.length >= 2 || parsed.coldLoad)) {
|
|
598
|
+
const bits = [
|
|
599
|
+
selected.length >= 2 ? "unloads each model before the next" : "",
|
|
600
|
+
parsed.coldLoad ? "unloads for --cold-load" : "",
|
|
601
|
+
].filter(Boolean);
|
|
602
|
+
console.error(`Note: benchmarking ${bits.join(" and ")} on ${machineLabel}.`);
|
|
616
603
|
}
|
|
617
604
|
console.error("Proceeding…");
|
|
618
605
|
}
|
|
606
|
+
if (parsed.hot) {
|
|
607
|
+
console.error("Hot: discarding the first throughput run; models stay loaded (no unload between models).");
|
|
608
|
+
}
|
|
619
609
|
if (parsed.settings.think === true) {
|
|
620
610
|
for (const name of selected) {
|
|
621
611
|
const caps = await fetchCapabilities(baseUrl, name);
|
|
@@ -665,18 +655,22 @@ export async function main() {
|
|
|
665
655
|
let modelError;
|
|
666
656
|
for (const caseDef of suiteCases) {
|
|
667
657
|
const attempts = [];
|
|
668
|
-
const
|
|
669
|
-
|
|
658
|
+
const discardFirst = parsed.hot && caseDef.role === "throughput";
|
|
659
|
+
const reps = caseDef.role === "throughput" ? parsed.runs + (discardFirst ? 1 : 0) : 1;
|
|
660
|
+
for (let i = 0; i < reps; i += 1) {
|
|
661
|
+
const discarded = discardFirst && i === 0;
|
|
670
662
|
const attempt = await runCaseAttempt({
|
|
671
663
|
baseUrl,
|
|
672
664
|
model,
|
|
673
665
|
caseDef,
|
|
674
|
-
run:
|
|
666
|
+
run: discarded ? 0 : discardFirst ? i : i + 1,
|
|
675
667
|
settings: baseSettings,
|
|
676
668
|
throughputNumPredict: parsed.throughputNumPredict,
|
|
677
669
|
judgeModel,
|
|
678
670
|
selfJudge,
|
|
679
671
|
});
|
|
672
|
+
if (discarded)
|
|
673
|
+
attempt.discarded = true;
|
|
680
674
|
attempts.push(attempt);
|
|
681
675
|
if (attempt.error) {
|
|
682
676
|
anyError = true;
|
|
@@ -717,8 +711,9 @@ export async function main() {
|
|
|
717
711
|
});
|
|
718
712
|
if (parsed.failFast && modelError)
|
|
719
713
|
break;
|
|
720
|
-
// Unload before next model when ≥2 models remain after this one / multi-model run
|
|
721
|
-
|
|
714
|
+
// Unload before next model when ≥2 models remain after this one / multi-model run.
|
|
715
|
+
// --hot keeps weights resident so the discarded first shot warms the rest.
|
|
716
|
+
if (!parsed.hot && selected.length >= 2 && mi < selected.length - 1) {
|
|
722
717
|
try {
|
|
723
718
|
await ollamaUnload(baseUrl, model, BENCH_TIMEOUT_MS);
|
|
724
719
|
await waitUntilUnloaded(baseUrl, model, UNLOAD_WAIT_MS);
|
|
@@ -755,6 +750,7 @@ export async function main() {
|
|
|
755
750
|
throughput_num_predict: parsed.throughputNumPredict,
|
|
756
751
|
num_ctx: baseSettings.num_ctx ?? null,
|
|
757
752
|
warmup: parsed.warmup,
|
|
753
|
+
hot: parsed.hot,
|
|
758
754
|
cold_load: parsed.coldLoad,
|
|
759
755
|
think: baseSettings.think === true,
|
|
760
756
|
bench_timeout_ms: BENCH_TIMEOUT_MS,
|
|
@@ -778,7 +774,7 @@ export async function main() {
|
|
|
778
774
|
}
|
|
779
775
|
else {
|
|
780
776
|
process.stdout.write("\n");
|
|
781
|
-
printTable(machineLabel, parsed.suite, parsed.runs, revision, compKey, modelResults, skipped, parsed.coldLoad);
|
|
777
|
+
printTable(machineLabel, parsed.suite, parsed.runs, revision, compKey, modelResults, skipped, parsed.coldLoad, parsed.hot);
|
|
782
778
|
if (parsed.save)
|
|
783
779
|
console.error(`benchmarks dir: ${benchmarksDir()}`);
|
|
784
780
|
}
|
package/dist/chats.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* ollanet chats --id a1b2c3d4e5f6
|
|
8
8
|
*/
|
|
9
9
|
import { loadChat, listChats, responsesDir, } from "./chat-store.js";
|
|
10
|
+
import { takeFlag } from "./argv.js";
|
|
10
11
|
function usage() {
|
|
11
12
|
console.error(`Usage: ollanet chats [--json] [--id <hash>]
|
|
12
13
|
|
|
@@ -31,12 +32,9 @@ function parseArgs(argv) {
|
|
|
31
32
|
json = true;
|
|
32
33
|
continue;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
console.error("--id requires a value");
|
|
38
|
-
usage();
|
|
39
|
-
}
|
|
35
|
+
const idFlag = takeFlag(arg, "--id", args, usage);
|
|
36
|
+
if (idFlag !== undefined) {
|
|
37
|
+
id = idFlag;
|
|
40
38
|
continue;
|
|
41
39
|
}
|
|
42
40
|
console.error(`Unknown flag: ${arg}`);
|
package/dist/cli.js
CHANGED
|
@@ -16,9 +16,11 @@ Discover Ollama hosts on localhost, your LAN, Tailscale, or any IP/hostname
|
|
|
16
16
|
you can reach — then chat with hash-addressed transcripts.
|
|
17
17
|
|
|
18
18
|
Usage:
|
|
19
|
-
ollanet scan [--json] [--all] [--lan]
|
|
19
|
+
ollanet scan [--json] [--all] [--lan] [--last]
|
|
20
20
|
ollanet prompt <machine|ip> [model] <prompt...>
|
|
21
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>]
|
|
22
24
|
ollanet pull <machine|ip> <model>
|
|
23
25
|
ollanet show <machine|ip> <model>
|
|
24
26
|
ollanet rm <machine|ip> <model> --yes
|
|
@@ -30,8 +32,11 @@ Usage:
|
|
|
30
32
|
Examples:
|
|
31
33
|
ollanet scan
|
|
32
34
|
ollanet scan --lan
|
|
35
|
+
ollanet scan --last # print the last saved scan (no network)
|
|
33
36
|
ollanet prompt localhost "What is MagicDNS?"
|
|
34
|
-
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"
|
|
35
40
|
ollanet prompt --chat a1b2c3d4e5f6 "Tell me more"
|
|
36
41
|
ollanet pull studio gemma3:12b
|
|
37
42
|
ollanet show studio gemma4-ctx32k
|
|
@@ -40,12 +45,14 @@ Examples:
|
|
|
40
45
|
ollanet chats
|
|
41
46
|
ollanet bench localhost --all
|
|
42
47
|
ollanet bench localhost llama3.2:1b --runs 3
|
|
48
|
+
ollanet bench localhost gemma3:12b --hot --runs 5
|
|
43
49
|
ollanet mcp # stdio MCP server for agents
|
|
44
50
|
|
|
45
51
|
Config: ~/.ollanet/config.json when installed, ./config.json in a checkout
|
|
46
52
|
(override with OLLANET_CONFIG)
|
|
47
53
|
Hosts: config.hosts, OLLANET_HOSTS, optional Tailscale + --lan
|
|
48
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)
|
|
49
56
|
`;
|
|
50
57
|
async function main() {
|
|
51
58
|
// pnpm/npm often forward a bare `--`; ignore it.
|
|
@@ -109,6 +116,13 @@ async function main() {
|
|
|
109
116
|
await run();
|
|
110
117
|
return;
|
|
111
118
|
}
|
|
119
|
+
case "compare":
|
|
120
|
+
case "vs":
|
|
121
|
+
case "h2h": {
|
|
122
|
+
const { main: run } = await import("./compare.js");
|
|
123
|
+
await run();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
112
126
|
case "bench":
|
|
113
127
|
case "benchmark": {
|
|
114
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
|
+
}>;
|