ollanet 0.6.2 → 0.6.4
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 +16 -439
- package/dist/alias.d.ts +9 -0
- package/dist/alias.js +147 -0
- package/dist/bench-suite.d.ts +8 -1
- package/dist/bench-suite.js +15 -8
- package/dist/bench.js +84 -27
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +16 -3
- package/dist/compare.js +12 -3
- package/dist/config.d.ts +23 -0
- package/dist/config.js +73 -1
- package/dist/help.js +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +3 -2
- package/dist/ollama-chat.d.ts +2 -0
- package/dist/prompt.js +22 -3
- package/dist/ps.d.ts +11 -0
- package/dist/ps.js +30 -4
- package/dist/pull.js +11 -5
- package/dist/rm.d.ts +1 -1
- package/dist/rm.js +11 -4
- package/dist/show.d.ts +1 -1
- package/dist/show.js +13 -3
- package/package.json +2 -1
package/dist/bench-suite.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Built-in ollanet bench suites, checkers, and revision hashes.
|
|
3
3
|
*/
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
+
/** Pinned length for `full`'s prose throughput case (`throughput_long`). */
|
|
6
|
+
export const LONG_THROUGHPUT_NUM_PREDICT = 1024;
|
|
5
7
|
const MATH_ANSWER = 323;
|
|
6
8
|
const REASON_ANSWER = 42;
|
|
7
9
|
function lastTokenNormalized(content) {
|
|
@@ -104,18 +106,23 @@ const FULL_EXTRA = [
|
|
|
104
106
|
{
|
|
105
107
|
id: "reason",
|
|
106
108
|
role: "check",
|
|
107
|
-
prompt: "
|
|
108
|
-
"How many apples are left? Reply with only the final number.",
|
|
109
|
+
prompt: "Start with 40. Add 7. Subtract 5. Reply with only the final number.",
|
|
109
110
|
num_predict: 48,
|
|
110
|
-
// 10 - 3 + 5 - 6 = 6... wait: gave away 2*3=6, so 10-3+5-6=6. Spec said 42 as placeholder.
|
|
111
|
-
// Use a problem that clearly ends at 42:
|
|
112
|
-
// Actually rewrite for 42: "Start with 40. Add 7. Subtract 5. Reply with only the number." => 42
|
|
113
111
|
check: checkExpectedInt(REASON_ANSWER),
|
|
114
112
|
},
|
|
113
|
+
{
|
|
114
|
+
id: "throughput_long",
|
|
115
|
+
role: "throughput",
|
|
116
|
+
// Chat-shaped tokens (markdown, lists, a table) at a long pin. Anti-EOS
|
|
117
|
+
// so done_reason=length; one shot (not --runs) so full stays usable.
|
|
118
|
+
prompt: "Write a detailed trade-off briefing for a product team choosing a web UI stack. " +
|
|
119
|
+
"Compare at least two options. Use markdown headings, bullet lists, and at least one table. " +
|
|
120
|
+
"Keep adding sections (testing, hiring, migration, failure modes, cost) without wrapping up. " +
|
|
121
|
+
"Do not write a closing recommendation that ends the piece. Do not stop early.",
|
|
122
|
+
num_predict: LONG_THROUGHPUT_NUM_PREDICT,
|
|
123
|
+
repeats: 1,
|
|
124
|
+
},
|
|
115
125
|
];
|
|
116
|
-
// Fix reason prompt to match REASON_ANSWER
|
|
117
|
-
FULL_EXTRA[FULL_EXTRA.length - 1].prompt =
|
|
118
|
-
"Start with 40. Add 7. Subtract 5. Reply with only the final number.";
|
|
119
126
|
export function getSuiteCases(suite) {
|
|
120
127
|
if (suite === "full")
|
|
121
128
|
return [...QUICK_CASES, ...FULL_EXTRA];
|
package/dist/bench.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ollanet bench — speed + lightweight quality suite against Ollama models.
|
|
3
3
|
*/
|
|
4
|
-
import { defaultModelForHost, loadConfig, machineSettingsForHost, mergeSettings, settingsFromEnv, } from "./config.js";
|
|
5
|
-
import { comparabilityKey, getSuiteCases, median, suiteRevision, } from "./bench-suite.js";
|
|
4
|
+
import { defaultModelForHost, loadConfig, lookupAlias, machineSettingsForHost, mergeSettings, settingsFromEnv, } from "./config.js";
|
|
5
|
+
import { comparabilityKey, getSuiteCases, LONG_THROUGHPUT_NUM_PREDICT, 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
8
|
import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
|
|
@@ -23,19 +23,20 @@ Examples:
|
|
|
23
23
|
ollanet bench studio gemma3:12b --hot --runs 5
|
|
24
24
|
ollanet help bench
|
|
25
25
|
|
|
26
|
-
Fixed suite (not compare). Checks run once; throughput repeats --runs times.
|
|
26
|
+
Fixed suite (not compare). Checks run once; 256-token throughput repeats --runs times.
|
|
27
27
|
Median tok/s excludes early-stops. Unload is between models, not between repeats.
|
|
28
|
+
full adds json/reason checks plus one 1024-token prose generation (tok/s long).
|
|
28
29
|
|
|
29
30
|
Options:
|
|
30
31
|
--all Every completion-capable model from /api/tags
|
|
31
32
|
--exclude-vision With --all, skip models that advertise vision
|
|
32
33
|
--suite quick|full Prompt suite (default quick)
|
|
33
|
-
--runs <n> Counted throughput repeats (default 3)
|
|
34
|
-
--hot Discard first throughput run; keep models loaded
|
|
34
|
+
--runs <n> Counted 256-token throughput repeats (default 3)
|
|
35
|
+
--hot Discard first 256-token throughput run; keep models loaded
|
|
35
36
|
--warmup Short discarded call before timed cases (default on)
|
|
36
37
|
--no-warmup Skip the short warmup (--hot implies this)
|
|
37
38
|
--cold-load Measure cold load via unload + /api/ps
|
|
38
|
-
--num-predict <n> Pin throughput length (default 256)
|
|
39
|
+
--num-predict <n> Pin 256-token throughput length (default 256; not the long case)
|
|
39
40
|
--num-ctx <n> Context window override
|
|
40
41
|
--keep-alive <value> Keep model loaded (e.g. 5m, 0, -1)
|
|
41
42
|
--think / --no-think Thinking tokens (default off)
|
|
@@ -65,6 +66,26 @@ function resolveModelName(tags, query) {
|
|
|
65
66
|
const prefix = tags.find((t) => t.name.toLowerCase().startsWith(`${q}:`) || q.startsWith(`${t.name.toLowerCase()}:`));
|
|
66
67
|
return prefix?.name;
|
|
67
68
|
}
|
|
69
|
+
function pinnedNumPredict(caseDef, throughputNumPredict) {
|
|
70
|
+
if (caseDef.role === "throughput") {
|
|
71
|
+
return caseDef.num_predict ?? throughputNumPredict;
|
|
72
|
+
}
|
|
73
|
+
return caseDef.num_predict ?? 32;
|
|
74
|
+
}
|
|
75
|
+
/** At least the global timeout; longer pins need room to finish at ~8 tok/s. */
|
|
76
|
+
function attemptTimeoutMs(numPredict) {
|
|
77
|
+
return Math.max(BENCH_TIMEOUT_MS, Math.ceil(numPredict / 8) * 1000);
|
|
78
|
+
}
|
|
79
|
+
function caseReps(caseDef, runs, hot) {
|
|
80
|
+
if (caseDef.role !== "throughput")
|
|
81
|
+
return 1;
|
|
82
|
+
if (caseDef.repeats != null)
|
|
83
|
+
return caseDef.repeats;
|
|
84
|
+
return runs + (hot ? 1 : 0);
|
|
85
|
+
}
|
|
86
|
+
function discardFirstThroughput(caseDef, hot) {
|
|
87
|
+
return hot && caseDef.role === "throughput" && caseDef.repeats == null;
|
|
88
|
+
}
|
|
68
89
|
function tokPerSec(chunk) {
|
|
69
90
|
if (chunk.eval_count == null || chunk.eval_duration == null || chunk.eval_duration <= 0) {
|
|
70
91
|
return undefined;
|
|
@@ -285,9 +306,7 @@ async function runColdLoad(baseUrl, model, keepAlive) {
|
|
|
285
306
|
}
|
|
286
307
|
}
|
|
287
308
|
async function runCaseAttempt(opts) {
|
|
288
|
-
const numPredict = opts.caseDef.
|
|
289
|
-
? opts.throughputNumPredict
|
|
290
|
-
: (opts.caseDef.num_predict ?? 32);
|
|
309
|
+
const numPredict = pinnedNumPredict(opts.caseDef, opts.throughputNumPredict);
|
|
291
310
|
const settings = mergeSettings(opts.settings, {
|
|
292
311
|
temperature: 0,
|
|
293
312
|
seed: 0,
|
|
@@ -303,7 +322,7 @@ async function runCaseAttempt(opts) {
|
|
|
303
322
|
messages: [{ role: "user", content: opts.caseDef.prompt }],
|
|
304
323
|
stream: false,
|
|
305
324
|
writeStdout: false,
|
|
306
|
-
timeoutMs:
|
|
325
|
+
timeoutMs: attemptTimeoutMs(numPredict),
|
|
307
326
|
settings,
|
|
308
327
|
});
|
|
309
328
|
const wallMs = Date.now() - wall0;
|
|
@@ -373,12 +392,23 @@ async function runCaseAttempt(opts) {
|
|
|
373
392
|
function countedAttempts(attempts) {
|
|
374
393
|
return attempts.filter((a) => !a.discarded);
|
|
375
394
|
}
|
|
376
|
-
function
|
|
377
|
-
const
|
|
378
|
-
const counted = countedAttempts(throughput?.attempts ?? []);
|
|
395
|
+
function speedFrom(cases, id) {
|
|
396
|
+
const counted = countedAttempts(cases.find((c) => c.id === id)?.attempts ?? []);
|
|
379
397
|
const fullLen = counted.filter((a) => !a.error && !a.early_stop && a.tok_s != null);
|
|
380
398
|
const toks = fullLen.map((a) => a.tok_s);
|
|
381
|
-
|
|
399
|
+
return {
|
|
400
|
+
median: median(toks) ?? null,
|
|
401
|
+
min: toks.length ? Math.min(...toks) : null,
|
|
402
|
+
max: toks.length ? Math.max(...toks) : null,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function summarizeModel(cases, cold, selfJudge) {
|
|
406
|
+
const peak = speedFrom(cases, "throughput");
|
|
407
|
+
const long = speedFrom(cases, "throughput_long");
|
|
408
|
+
const throughputAttempts = cases
|
|
409
|
+
.filter((c) => c.role === "throughput")
|
|
410
|
+
.flatMap((c) => countedAttempts(c.attempts));
|
|
411
|
+
const earlyStopCount = throughputAttempts.filter((a) => a.early_stop || (a.done_reason && a.done_reason !== "length")).length;
|
|
382
412
|
const checkAttempts = cases
|
|
383
413
|
.filter((c) => c.role === "check")
|
|
384
414
|
.flatMap((c) => c.attempts)
|
|
@@ -386,9 +416,12 @@ function summarizeModel(cases, cold, selfJudge) {
|
|
|
386
416
|
const passed = checkAttempts.filter((a) => a.quality?.pass).length;
|
|
387
417
|
const passRate = checkAttempts.length > 0 ? passed / checkAttempts.length : null;
|
|
388
418
|
return {
|
|
389
|
-
tok_s_median: median
|
|
390
|
-
tok_s_min:
|
|
391
|
-
tok_s_max:
|
|
419
|
+
tok_s_median: peak.median,
|
|
420
|
+
tok_s_min: peak.min,
|
|
421
|
+
tok_s_max: peak.max,
|
|
422
|
+
tok_s_long_median: long.median,
|
|
423
|
+
tok_s_long_min: long.min,
|
|
424
|
+
tok_s_long_max: long.max,
|
|
392
425
|
early_stop_count: earlyStopCount,
|
|
393
426
|
pass_rate: passRate,
|
|
394
427
|
load_ms: cold && cold.error == null && cold.load_ms > 0 ? cold.load_ms : cold?.error ? null : cold?.load_ms ?? null,
|
|
@@ -404,17 +437,22 @@ function printTable(machine, suite, runs, revision, compKey, models, skipped, co
|
|
|
404
437
|
lines.push("Cold-load: on");
|
|
405
438
|
if (hot)
|
|
406
439
|
lines.push("Hot: first throughput run discarded; models stay loaded");
|
|
440
|
+
if (suite === "full")
|
|
441
|
+
lines.push("full: tok/s long is one 1024-token prose generation");
|
|
407
442
|
lines.push("");
|
|
443
|
+
const showLong = suite === "full";
|
|
408
444
|
const header = coldLoad
|
|
409
445
|
? pad("Model", MODEL_COL) +
|
|
410
446
|
pad("tok/s (med)", 12) +
|
|
411
447
|
pad("spread", 12) +
|
|
448
|
+
(showLong ? pad("tok/s long", 12) : "") +
|
|
412
449
|
pad("load", 8) +
|
|
413
450
|
pad("pass", 8) +
|
|
414
451
|
"notes"
|
|
415
452
|
: pad("Model", MODEL_COL) +
|
|
416
453
|
pad("tok/s (med)", 12) +
|
|
417
454
|
pad("spread", 12) +
|
|
455
|
+
(showLong ? pad("tok/s long", 12) : "") +
|
|
418
456
|
pad("pass", 8) +
|
|
419
457
|
"notes";
|
|
420
458
|
lines.push(header);
|
|
@@ -448,6 +486,7 @@ function printTable(machine, suite, runs, revision, compKey, models, skipped, co
|
|
|
448
486
|
const spread = m.summary.tok_s_min != null && m.summary.tok_s_max != null
|
|
449
487
|
? `${m.summary.tok_s_min.toFixed(1)}–${m.summary.tok_s_max.toFixed(1)}`
|
|
450
488
|
: "—";
|
|
489
|
+
const tokLong = m.summary.tok_s_long_median != null ? m.summary.tok_s_long_median.toFixed(1) : "—";
|
|
451
490
|
const pass = m.summary.pass_rate != null
|
|
452
491
|
? `${Math.round(m.summary.pass_rate * checkDenom(m))}/${checkDenom(m)}`
|
|
453
492
|
: "—";
|
|
@@ -467,12 +506,14 @@ function printTable(machine, suite, runs, revision, compKey, models, skipped, co
|
|
|
467
506
|
? pad(m.name, MODEL_COL) +
|
|
468
507
|
pad(tok, 12) +
|
|
469
508
|
pad(spread, 12) +
|
|
509
|
+
(showLong ? pad(tokLong, 12) : "") +
|
|
470
510
|
pad(load, 8) +
|
|
471
511
|
pad(pass, 8) +
|
|
472
512
|
notes.join("; ")
|
|
473
513
|
: pad(m.name, MODEL_COL) +
|
|
474
514
|
pad(tok, 12) +
|
|
475
515
|
pad(spread, 12) +
|
|
516
|
+
(showLong ? pad(tokLong, 12) : "") +
|
|
476
517
|
pad(pass, 8) +
|
|
477
518
|
notes.join("; ");
|
|
478
519
|
lines.push(row);
|
|
@@ -501,7 +542,16 @@ function pad(s, n) {
|
|
|
501
542
|
export async function main() {
|
|
502
543
|
const parsed = parseArgs(process.argv.slice(2));
|
|
503
544
|
const config = await loadConfig();
|
|
504
|
-
const
|
|
545
|
+
const alias = lookupAlias(config, parsed.machine);
|
|
546
|
+
let machineQuery = parsed.machine;
|
|
547
|
+
let models = [...parsed.models];
|
|
548
|
+
if (alias) {
|
|
549
|
+
machineQuery = alias.machine;
|
|
550
|
+
if (models.length === 0 && !parsed.all) {
|
|
551
|
+
models = [alias.model];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const host = await resolveTarget(machineQuery, config);
|
|
505
555
|
const baseUrl = ollamaBaseUrl(host);
|
|
506
556
|
const machineLabel = shortName(host);
|
|
507
557
|
const tags = await ollamaTags(baseUrl, Math.min(BENCH_TIMEOUT_MS, 15_000));
|
|
@@ -528,8 +578,8 @@ export async function main() {
|
|
|
528
578
|
selected.push(t.name);
|
|
529
579
|
}
|
|
530
580
|
}
|
|
531
|
-
else if (
|
|
532
|
-
for (const q of
|
|
581
|
+
else if (models.length > 0) {
|
|
582
|
+
for (const q of models) {
|
|
533
583
|
const resolved = resolveModelName(tags, q);
|
|
534
584
|
if (!resolved) {
|
|
535
585
|
console.error(`Unknown model "${q}". Available: ${tagNames.join(", ")}`);
|
|
@@ -546,7 +596,7 @@ export async function main() {
|
|
|
546
596
|
else {
|
|
547
597
|
const def = defaultModelForHost(config, host);
|
|
548
598
|
if (!def) {
|
|
549
|
-
console.error(`No model specified for "${machineLabel}". Pass model names, use --all, or
|
|
599
|
+
console.error(`No model specified for "${machineLabel}". Pass model names, use --all, set defaultModels, or use an alias.`);
|
|
550
600
|
process.exit(1);
|
|
551
601
|
}
|
|
552
602
|
const name = resolveModelName(tags, def);
|
|
@@ -585,10 +635,13 @@ export async function main() {
|
|
|
585
635
|
hot: parsed.hot,
|
|
586
636
|
});
|
|
587
637
|
const suiteCases = getSuiteCases(parsed.suite);
|
|
588
|
-
const throughputReps = parsed.runs + (parsed.hot ? 1 : 0);
|
|
589
638
|
const caseCount = selected.length *
|
|
590
|
-
|
|
591
|
-
|
|
639
|
+
suiteCases.reduce((n, c) => n + caseReps(c, parsed.runs, parsed.hot), 0);
|
|
640
|
+
const worstCaseMs = selected.length *
|
|
641
|
+
suiteCases.reduce((n, c) => {
|
|
642
|
+
const pin = pinnedNumPredict(c, parsed.throughputNumPredict);
|
|
643
|
+
return n + caseReps(c, parsed.runs, parsed.hot) * attemptTimeoutMs(pin);
|
|
644
|
+
}, 0);
|
|
592
645
|
if (!parsed.json) {
|
|
593
646
|
const skipBits = [
|
|
594
647
|
skipped.filter((s) => s.reason === "non-completion").length
|
|
@@ -603,7 +656,10 @@ export async function main() {
|
|
|
603
656
|
` × suite=${parsed.suite} × runs=${parsed.runs}` +
|
|
604
657
|
(parsed.hot ? " (hot: +1 discarded)" : ""));
|
|
605
658
|
console.error(`Cases: ~${caseCount} chat calls timeout: ${BENCH_TIMEOUT_MS}ms/case` +
|
|
606
|
-
|
|
659
|
+
(parsed.suite === "full"
|
|
660
|
+
? ` (long case ≥${attemptTimeoutMs(LONG_THROUGHPUT_NUM_PREDICT)}ms)`
|
|
661
|
+
: "") +
|
|
662
|
+
` worst-case ceiling: ~${Math.ceil(worstCaseMs / 60000)}m if every call times out`);
|
|
607
663
|
if (!parsed.hot && !isLoopback(host) && (selected.length >= 2 || parsed.coldLoad)) {
|
|
608
664
|
const bits = [
|
|
609
665
|
selected.length >= 2 ? "unloads each model before the next" : "",
|
|
@@ -665,8 +721,8 @@ export async function main() {
|
|
|
665
721
|
let modelError;
|
|
666
722
|
for (const caseDef of suiteCases) {
|
|
667
723
|
const attempts = [];
|
|
668
|
-
const discardFirst = parsed.hot
|
|
669
|
-
const reps = caseDef
|
|
724
|
+
const discardFirst = discardFirstThroughput(caseDef, parsed.hot);
|
|
725
|
+
const reps = caseReps(caseDef, parsed.runs, parsed.hot);
|
|
670
726
|
for (let i = 0; i < reps; i += 1) {
|
|
671
727
|
const discarded = discardFirst && i === 0;
|
|
672
728
|
const attempt = await runCaseAttempt({
|
|
@@ -758,6 +814,7 @@ export async function main() {
|
|
|
758
814
|
temperature: 0,
|
|
759
815
|
seed: 0,
|
|
760
816
|
throughput_num_predict: parsed.throughputNumPredict,
|
|
817
|
+
throughput_long_num_predict: parsed.suite === "full" ? LONG_THROUGHPUT_NUM_PREDICT : null,
|
|
761
818
|
num_ctx: baseSettings.num_ctx ?? null,
|
|
762
819
|
warmup: parsed.warmup,
|
|
763
820
|
hot: parsed.hot,
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* ollanet —
|
|
3
|
+
* ollanet — Ollama over any reachable network (CLI, MCP, library).
|
|
4
4
|
*
|
|
5
5
|
* ollanet scan
|
|
6
6
|
* ollanet prompt <machine-or-ip> "hello"
|
|
7
|
+
* ollanet compare <machine> <model> <model>
|
|
7
8
|
* ollanet pull <machine> <model>
|
|
8
9
|
* ollanet show <machine> <model>
|
|
9
10
|
* ollanet rm <machine> <model> --yes
|
|
10
11
|
* ollanet ps [machine]
|
|
12
|
+
* ollanet bench <machine> [model...]
|
|
11
13
|
* ollanet chats
|
|
14
|
+
* ollanet mcp
|
|
12
15
|
*/
|
|
13
16
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* ollanet —
|
|
3
|
+
* ollanet — Ollama over any reachable network (CLI, MCP, library).
|
|
4
4
|
*
|
|
5
5
|
* ollanet scan
|
|
6
6
|
* ollanet prompt <machine-or-ip> "hello"
|
|
7
|
+
* ollanet compare <machine> <model> <model>
|
|
7
8
|
* ollanet pull <machine> <model>
|
|
8
9
|
* ollanet show <machine> <model>
|
|
9
10
|
* ollanet rm <machine> <model> --yes
|
|
10
11
|
* ollanet ps [machine]
|
|
12
|
+
* ollanet bench <machine> [model...]
|
|
11
13
|
* ollanet chats
|
|
14
|
+
* ollanet mcp
|
|
12
15
|
*/
|
|
13
16
|
const HELP = `ollanet — Ollama over your network
|
|
14
17
|
|
|
15
|
-
Discover
|
|
16
|
-
|
|
18
|
+
Discover hosts, manage models, prompt and compare, bench tok/s, and continue
|
|
19
|
+
chats by hash. CLI, MCP, and a Node library — same mesh.
|
|
17
20
|
|
|
18
21
|
Usage:
|
|
19
22
|
ollanet scan [--json] [--all] [--lan] [--last]
|
|
@@ -27,6 +30,7 @@ Usage:
|
|
|
27
30
|
ollanet ps [machine]
|
|
28
31
|
ollanet chats [--json] [--id <hash>]
|
|
29
32
|
ollanet bench <machine|ip> [model...] [--all] [options]
|
|
33
|
+
ollanet alias list|add|rm …
|
|
30
34
|
ollanet mcp
|
|
31
35
|
ollanet help [command]
|
|
32
36
|
|
|
@@ -44,6 +48,8 @@ Examples:
|
|
|
44
48
|
ollanet rm studio llama3.2:1b --yes
|
|
45
49
|
ollanet ps studio
|
|
46
50
|
ollanet chats
|
|
51
|
+
ollanet alias add desk studio gemma3:12b
|
|
52
|
+
ollanet prompt desk "hello" # expands machine + model
|
|
47
53
|
ollanet bench localhost --all
|
|
48
54
|
ollanet bench localhost llama3.2:1b --runs 3
|
|
49
55
|
ollanet bench localhost gemma3:12b --hot --runs 5
|
|
@@ -53,6 +59,7 @@ Examples:
|
|
|
53
59
|
Config: ~/.ollanet/config.json when installed, ./config.json in a checkout
|
|
54
60
|
(override with OLLANET_CONFIG)
|
|
55
61
|
Hosts: config.hosts, OLLANET_HOSTS, optional Tailscale + --lan
|
|
62
|
+
Aliases: config.aliases — ollanet alias add|list|rm
|
|
56
63
|
Chats: ~/.ollanet/responses or ./responses (override with OLLANET_RESPONSES_DIR)
|
|
57
64
|
Scan: ~/.ollanet/last-scan.json or ./last-scan.json (override with OLLANET_LAST_SCAN)
|
|
58
65
|
`;
|
|
@@ -162,6 +169,12 @@ async function main() {
|
|
|
162
169
|
await run();
|
|
163
170
|
return;
|
|
164
171
|
}
|
|
172
|
+
case "alias":
|
|
173
|
+
case "aliases": {
|
|
174
|
+
const { main: run } = await import("./alias.js");
|
|
175
|
+
await run();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
165
178
|
default:
|
|
166
179
|
console.error(`Unknown command: ${cmd}\n`);
|
|
167
180
|
process.stderr.write(HELP);
|
package/dist/compare.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* ollanet compare studio gemma3:12b llama3.2:3b --prompt "Explain MagicDNS"
|
|
5
5
|
* ollanet compare studio m1 m2 --file task.md
|
|
6
6
|
*/
|
|
7
|
-
import { configFromPartial, loadConfig, machineSettingsForHost, mergeSettings, settingsFromEnv, } from "./config.js";
|
|
7
|
+
import { configFromPartial, loadConfig, lookupAlias, machineSettingsForHost, mergeSettings, settingsFromEnv, } from "./config.js";
|
|
8
8
|
import { envInt, ollamaBaseUrl, shortName } from "./hosts.js";
|
|
9
9
|
import { consumeSettingsFlag, failUsage, isHelpFlag, printHelp, takeFlag, takeValue } from "./argv.js";
|
|
10
10
|
import { resolveTarget } from "./target.js";
|
|
@@ -244,6 +244,14 @@ export async function main() {
|
|
|
244
244
|
console.error("Machine is required.");
|
|
245
245
|
usage();
|
|
246
246
|
}
|
|
247
|
+
const config = await loadConfig();
|
|
248
|
+
const alias = lookupAlias(config, parsed.machine);
|
|
249
|
+
const machine = alias ? alias.machine : parsed.machine;
|
|
250
|
+
let models = [...parsed.models];
|
|
251
|
+
if (alias && models.length === 1) {
|
|
252
|
+
// `compare desk other:7b` → alias model vs other on alias host
|
|
253
|
+
models = [alias.model, models[0]];
|
|
254
|
+
}
|
|
247
255
|
const { stdin: stdinStream } = await import("node:process");
|
|
248
256
|
let fromStdin = "";
|
|
249
257
|
if (!stdinStream.isTTY) {
|
|
@@ -265,14 +273,15 @@ export async function main() {
|
|
|
265
273
|
}
|
|
266
274
|
const prompt = assemblePrompt({ argv: parsed.prompt, file: fileText, stdin: fromStdin });
|
|
267
275
|
const record = await runCompare({
|
|
268
|
-
machine
|
|
269
|
-
models
|
|
276
|
+
machine,
|
|
277
|
+
models,
|
|
270
278
|
prompt: prompt || undefined,
|
|
271
279
|
settings: parsed.settings,
|
|
272
280
|
save: parsed.save,
|
|
273
281
|
unload: parsed.unload,
|
|
274
282
|
writeStdout: !parsed.json,
|
|
275
283
|
quiet: parsed.json,
|
|
284
|
+
config,
|
|
276
285
|
});
|
|
277
286
|
if (record.default_prompt) {
|
|
278
287
|
process.stderr.write("Using default compare prompt (pass --prompt or --file to override).\n");
|
package/dist/config.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface GenerateSettings {
|
|
|
17
17
|
*/
|
|
18
18
|
think?: boolean;
|
|
19
19
|
}
|
|
20
|
+
/** Named shortcut for a frequent machine + model pair. */
|
|
21
|
+
export interface ModelAlias {
|
|
22
|
+
machine: string;
|
|
23
|
+
model: string;
|
|
24
|
+
}
|
|
20
25
|
export interface AppConfig {
|
|
21
26
|
/** Explicit hosts/IPs to probe (LAN, VPN, MagicDNS, etc.). */
|
|
22
27
|
hosts: HostConfigEntry[];
|
|
@@ -28,10 +33,28 @@ export interface AppConfig {
|
|
|
28
33
|
machineDefaults: Record<string, GenerateSettings & {
|
|
29
34
|
model?: string;
|
|
30
35
|
}>;
|
|
36
|
+
/** Shortcuts: `ollanet prompt <alias> "…"` expands to machine + model. */
|
|
37
|
+
aliases: Record<string, ModelAlias>;
|
|
31
38
|
}
|
|
32
39
|
/** Build an AppConfig from an in-memory partial (no file I/O). */
|
|
33
40
|
export declare function configFromPartial(partial?: Partial<AppConfig>): AppConfig;
|
|
34
41
|
export declare function loadConfig(): Promise<AppConfig>;
|
|
42
|
+
/**
|
|
43
|
+
* Read-modify-write the config JSON on disk. Preserves unknown keys.
|
|
44
|
+
* Creates the parent directory when missing (installed `~/.ollanet/`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function mutateConfigFile(mutate: (file: Record<string, unknown>) => void): Promise<void>;
|
|
47
|
+
/** Alias names: letter start, then letters/digits/_/- (no colons — those look like models). */
|
|
48
|
+
export declare function isValidAliasName(name: string): boolean;
|
|
49
|
+
export declare function lookupAlias(config: AppConfig, name: string | undefined): ModelAlias | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* If `machine` is an alias name, expand to that alias's host.
|
|
52
|
+
* When `model` is omitted, fill it from the alias too.
|
|
53
|
+
*/
|
|
54
|
+
export declare function expandMachineModel(config: AppConfig, machine: string | undefined, model: string | undefined): {
|
|
55
|
+
machine?: string;
|
|
56
|
+
model?: string;
|
|
57
|
+
};
|
|
35
58
|
export declare function normalizeSettings(input: unknown): GenerateSettings;
|
|
36
59
|
export declare function parseFormat(value: unknown): GenerateSettings["format"] | undefined;
|
|
37
60
|
export declare function hostLookupKeys(host: HostTarget): string[];
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { shortName, } from "./hosts.js";
|
|
3
4
|
import { defaultConfigPath } from "./paths.js";
|
|
4
5
|
function emptyConfig() {
|
|
@@ -8,6 +9,7 @@ function emptyConfig() {
|
|
|
8
9
|
defaultModels: {},
|
|
9
10
|
defaults: {},
|
|
10
11
|
machineDefaults: {},
|
|
12
|
+
aliases: {},
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
15
|
/** Build an AppConfig from an in-memory partial (no file I/O). */
|
|
@@ -19,6 +21,7 @@ export function configFromPartial(partial = {}) {
|
|
|
19
21
|
defaultModels: partial.defaultModels ?? empty.defaultModels,
|
|
20
22
|
defaults: partial.defaults ?? empty.defaults,
|
|
21
23
|
machineDefaults: partial.machineDefaults ?? empty.machineDefaults,
|
|
24
|
+
aliases: partial.aliases ?? empty.aliases,
|
|
22
25
|
};
|
|
23
26
|
}
|
|
24
27
|
const CONFIG_PATH = process.env.OLLANET_CONFIG ??
|
|
@@ -42,6 +45,7 @@ export async function loadConfig() {
|
|
|
42
45
|
defaultModels,
|
|
43
46
|
defaults: normalizeSettings(parsed.defaults),
|
|
44
47
|
machineDefaults,
|
|
48
|
+
aliases: normalizeAliases(parsed.aliases),
|
|
45
49
|
};
|
|
46
50
|
}
|
|
47
51
|
catch (err) {
|
|
@@ -52,6 +56,29 @@ export async function loadConfig() {
|
|
|
52
56
|
throw new Error(`Failed to read config at ${CONFIG_PATH}: ${err instanceof Error ? err.message : err}`);
|
|
53
57
|
}
|
|
54
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Read-modify-write the config JSON on disk. Preserves unknown keys.
|
|
61
|
+
* Creates the parent directory when missing (installed `~/.ollanet/`).
|
|
62
|
+
*/
|
|
63
|
+
export async function mutateConfigFile(mutate) {
|
|
64
|
+
let file = {};
|
|
65
|
+
try {
|
|
66
|
+
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
69
|
+
file = parsed;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const code = err.code;
|
|
74
|
+
if (code !== "ENOENT") {
|
|
75
|
+
throw new Error(`Failed to read config at ${CONFIG_PATH}: ${err instanceof Error ? err.message : err}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
mutate(file);
|
|
79
|
+
await mkdir(path.dirname(CONFIG_PATH), { recursive: true });
|
|
80
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify(file, null, 2)}\n`, "utf8");
|
|
81
|
+
}
|
|
55
82
|
function normalizeDiscovery(input) {
|
|
56
83
|
if (!input || typeof input !== "object")
|
|
57
84
|
return {};
|
|
@@ -77,6 +104,51 @@ function normalizeStringMap(map) {
|
|
|
77
104
|
}
|
|
78
105
|
return out;
|
|
79
106
|
}
|
|
107
|
+
function normalizeAliases(map) {
|
|
108
|
+
if (!map || typeof map !== "object" || Array.isArray(map))
|
|
109
|
+
return {};
|
|
110
|
+
const out = {};
|
|
111
|
+
for (const [key, value] of Object.entries(map)) {
|
|
112
|
+
const name = key.trim().toLowerCase();
|
|
113
|
+
if (!name || !value || typeof value !== "object" || Array.isArray(value))
|
|
114
|
+
continue;
|
|
115
|
+
const row = value;
|
|
116
|
+
const machine = typeof row.machine === "string" ? row.machine.trim() : "";
|
|
117
|
+
const model = typeof row.model === "string" ? row.model.trim() : "";
|
|
118
|
+
if (!machine || !model)
|
|
119
|
+
continue;
|
|
120
|
+
out[name] = { machine, model };
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** Alias names: letter start, then letters/digits/_/- (no colons — those look like models). */
|
|
125
|
+
export function isValidAliasName(name) {
|
|
126
|
+
return /^[a-z][a-z0-9_-]*$/i.test(name.trim());
|
|
127
|
+
}
|
|
128
|
+
export function lookupAlias(config, name) {
|
|
129
|
+
if (!name?.trim())
|
|
130
|
+
return undefined;
|
|
131
|
+
return config.aliases[name.trim().toLowerCase()];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* If `machine` is an alias name, expand to that alias's host.
|
|
135
|
+
* When `model` is omitted, fill it from the alias too.
|
|
136
|
+
*/
|
|
137
|
+
export function expandMachineModel(config, machine, model) {
|
|
138
|
+
if (!machine?.trim())
|
|
139
|
+
return { machine, model };
|
|
140
|
+
const alias = lookupAlias(config, machine);
|
|
141
|
+
if (!alias) {
|
|
142
|
+
return {
|
|
143
|
+
machine: machine.trim(),
|
|
144
|
+
model: model?.trim() ? model.trim() : undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
machine: alias.machine,
|
|
149
|
+
model: model?.trim() ? model.trim() : alias.model,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
80
152
|
function normalizeMachineDefaults(map) {
|
|
81
153
|
const out = {};
|
|
82
154
|
for (const [key, value] of Object.entries(map ?? {})) {
|
package/dist/help.js
CHANGED
|
@@ -11,6 +11,7 @@ const LOADERS = {
|
|
|
11
11
|
ps: () => import("./ps.js"),
|
|
12
12
|
compare: () => import("./compare.js"),
|
|
13
13
|
bench: () => import("./bench.js"),
|
|
14
|
+
alias: () => import("./alias.js"),
|
|
14
15
|
mcp: () => import("./mcp.js"),
|
|
15
16
|
};
|
|
16
17
|
const ALIASES = {
|
|
@@ -26,6 +27,7 @@ const ALIASES = {
|
|
|
26
27
|
vs: "compare",
|
|
27
28
|
h2h: "compare",
|
|
28
29
|
benchmark: "bench",
|
|
30
|
+
aliases: "alias",
|
|
29
31
|
};
|
|
30
32
|
export function canonicalCommand(name) {
|
|
31
33
|
const key = name.trim().toLowerCase();
|
package/dist/index.d.ts
CHANGED
|
@@ -13,8 +13,8 @@ export { lastScan, lastScanPath, loadLastScan } from "./scan-store.js";
|
|
|
13
13
|
export type { StoredScan } from "./scan-store.js";
|
|
14
14
|
export { discoverHosts, resolveHost, ollamaBaseUrl, shortName, ollamaPort, } from "./hosts.js";
|
|
15
15
|
export type { DiscoverOptions, DiscoveryResult, HostTarget, HostSource, HostConfigEntry, } from "./hosts.js";
|
|
16
|
-
export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, } from "./config.js";
|
|
17
|
-
export type { AppConfig, DiscoveryConfig, GenerateSettings } from "./config.js";
|
|
16
|
+
export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, lookupAlias, expandMachineModel, } from "./config.js";
|
|
17
|
+
export type { AppConfig, DiscoveryConfig, GenerateSettings, ModelAlias } from "./config.js";
|
|
18
18
|
export { runPrompt } from "./prompt.js";
|
|
19
19
|
export type { PromptRunOptions, PromptRunResult } from "./prompt.js";
|
|
20
20
|
export { runCompare, DEFAULT_COMPARE_PROMPT } from "./compare.js";
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export { scanNetwork } from "./scan.js";
|
|
11
11
|
export { lastScan, lastScanPath, loadLastScan } from "./scan-store.js";
|
|
12
12
|
export { discoverHosts, resolveHost, ollamaBaseUrl, shortName, ollamaPort, } from "./hosts.js";
|
|
13
|
-
export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, } from "./config.js";
|
|
13
|
+
export { loadConfig, defaultModelForHost, mergeSettings, configFromPartial, lookupAlias, expandMachineModel, } from "./config.js";
|
|
14
14
|
export { runPrompt } from "./prompt.js";
|
|
15
15
|
export { runCompare, DEFAULT_COMPARE_PROMPT } from "./compare.js";
|
|
16
16
|
export { assemblePrompt, readPromptFile } from "./prompt-input.js";
|
package/dist/mcp.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* ollanet mcp — stdio MCP server (zero deps).
|
|
3
3
|
*
|
|
4
4
|
* Speaks the Model Context Protocol over stdin/stdout so agents (Cursor,
|
|
5
|
-
* Claude Desktop, etc.) can discover hosts,
|
|
5
|
+
* Claude Desktop, etc.) can discover hosts, manage models, prompt, compare,
|
|
6
|
+
* and continue chats.
|
|
6
7
|
*
|
|
7
8
|
* ollanet mcp
|
|
8
9
|
*
|
package/dist/mcp.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* ollanet mcp — stdio MCP server (zero deps).
|
|
3
3
|
*
|
|
4
4
|
* Speaks the Model Context Protocol over stdin/stdout so agents (Cursor,
|
|
5
|
-
* Claude Desktop, etc.) can discover hosts,
|
|
5
|
+
* Claude Desktop, etc.) can discover hosts, manage models, prompt, compare,
|
|
6
|
+
* and continue chats.
|
|
6
7
|
*
|
|
7
8
|
* ollanet mcp
|
|
8
9
|
*
|
|
@@ -200,7 +201,7 @@ const TOOLS = [
|
|
|
200
201
|
{
|
|
201
202
|
name: "ollanet_ps",
|
|
202
203
|
description: "List models currently loaded in VRAM. Optional machine; omit to probe every discovered host. " +
|
|
203
|
-
"scan is on-disk inventory; ps is what is resident right now
|
|
204
|
+
"scan is on-disk inventory; ps is what is resident right now, with CPU/GPU %.",
|
|
204
205
|
inputSchema: {
|
|
205
206
|
type: "object",
|
|
206
207
|
properties: {
|
package/dist/ollama-chat.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export declare function ollamaVersion(baseUrl: string, timeoutMs: number): Promi
|
|
|
74
74
|
export interface PsModel {
|
|
75
75
|
name?: string;
|
|
76
76
|
model?: string;
|
|
77
|
+
/** Total resident bytes (VRAM + CPU/RAM). */
|
|
78
|
+
size?: number;
|
|
77
79
|
size_vram?: number;
|
|
78
80
|
context_length?: number;
|
|
79
81
|
digest?: string;
|