ollanet 0.6.2 → 0.6.3
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 +7 -5
- package/dist/bench-suite.d.ts +8 -1
- package/dist/bench-suite.js +15 -8
- package/dist/bench.js +70 -22
- package/dist/mcp.js +1 -1
- package/dist/ollama-chat.d.ts +2 -0
- package/dist/ps.d.ts +11 -0
- package/dist/ps.js +23 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -192,7 +192,7 @@ optimizeDeps: { exclude: ["ollanet"] },
|
|
|
192
192
|
| `ollanet pull <machine> <model>` | Ask that host to download / update a library model |
|
|
193
193
|
| `ollanet show <machine> <model>` | Inspect Modelfile / params / capabilities (`[tuned]` when it looks like Finetuna) |
|
|
194
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) |
|
|
195
|
+
| `ollanet ps [machine]` | Models loaded in VRAM, with CPU/GPU % (omit machine = every discovered host) |
|
|
196
196
|
| `ollanet prompt …` | Send a prompt / continue a chat (`--file` for `.txt` / `.md`) |
|
|
197
197
|
| `ollanet compare …` | Same prompt on 2–5 models; summary + `compares/<id>.md` |
|
|
198
198
|
| `ollanet chats` | List or inspect saved transcripts |
|
|
@@ -340,10 +340,12 @@ ollanet bench localhost gemma4:12b --hot --runs 5
|
|
|
340
340
|
```
|
|
341
341
|
|
|
342
342
|
Runs a built-in suite (`quick` default, or `--suite full`) against one or more **completion**
|
|
343
|
-
models: instruction checks once, throughput
|
|
343
|
+
models: instruction checks once, 256-token throughput repeated (`--runs`, default 3) with
|
|
344
344
|
pinned `num_predict` / `seed` / `temperature`. Reports median tok/s + spread.
|
|
345
|
-
`--hot` discards the first
|
|
346
|
-
repeats are already warm (Finetuna steady-state).
|
|
345
|
+
`--hot` discards the first 256-token run and leaves models loaded so the counted
|
|
346
|
+
repeats are already warm (Finetuna steady-state). `--suite full` also runs json/reason
|
|
347
|
+
checks and **one** 1024-token prose generation (`tok/s long`) — closer to a long chat
|
|
348
|
+
reply. Under `--all`,
|
|
347
349
|
skips non-completion models (embeddings); multimodal `completion`+`vision` models stay in
|
|
348
350
|
(early-stop / pass_rate handle text-suite failures). Use `--exclude-vision` to drop them.
|
|
349
351
|
Saves `benchmarks/<id>.json` (or `~/.ollanet/benchmarks/` when installed).
|
|
@@ -351,7 +353,7 @@ See [`docs/bench-spec.md`](docs/bench-spec.md) for measurement rules.
|
|
|
351
353
|
|
|
352
354
|
`quick` has only two scored checks (`ping`, `math`), so `pass` is a coarse
|
|
353
355
|
liveness/sanity gauge (`0/2`, `1/2`, `2/2`) — not “50% broken.” Use `--suite full`
|
|
354
|
-
when you care more about the quality signal.
|
|
356
|
+
when you care more about the quality signal or want the long tok/s column.
|
|
355
357
|
|
|
356
358
|
## Discovery
|
|
357
359
|
|
package/dist/bench-suite.d.ts
CHANGED
|
@@ -3,14 +3,21 @@ export interface BenchCase {
|
|
|
3
3
|
id: string;
|
|
4
4
|
role: CaseRole;
|
|
5
5
|
prompt: string;
|
|
6
|
-
/** Cap for this case
|
|
6
|
+
/** Cap for this case. Peak throughput uses `--num-predict` when omitted. */
|
|
7
7
|
num_predict?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Throughput attempts. Omit to use `--runs` (and `--hot` discard).
|
|
10
|
+
* Set `1` for a single large shot that does not repeat.
|
|
11
|
+
*/
|
|
12
|
+
repeats?: number;
|
|
8
13
|
format?: "json";
|
|
9
14
|
check?: (content: string) => {
|
|
10
15
|
ok: boolean;
|
|
11
16
|
detail?: string;
|
|
12
17
|
};
|
|
13
18
|
}
|
|
19
|
+
/** Pinned length for `full`'s prose throughput case (`throughput_long`). */
|
|
20
|
+
export declare const LONG_THROUGHPUT_NUM_PREDICT = 1024;
|
|
14
21
|
export type SuiteName = "quick" | "full";
|
|
15
22
|
export declare function getSuiteCases(suite: SuiteName): BenchCase[];
|
|
16
23
|
export declare function suiteRevision(suite: SuiteName): string;
|
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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ollanet bench — speed + lightweight quality suite against Ollama models.
|
|
3
3
|
*/
|
|
4
4
|
import { defaultModelForHost, loadConfig, machineSettingsForHost, mergeSettings, settingsFromEnv, } from "./config.js";
|
|
5
|
-
import { comparabilityKey, getSuiteCases, median, suiteRevision, } from "./bench-suite.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);
|
|
@@ -585,10 +626,13 @@ export async function main() {
|
|
|
585
626
|
hot: parsed.hot,
|
|
586
627
|
});
|
|
587
628
|
const suiteCases = getSuiteCases(parsed.suite);
|
|
588
|
-
const throughputReps = parsed.runs + (parsed.hot ? 1 : 0);
|
|
589
629
|
const caseCount = selected.length *
|
|
590
|
-
|
|
591
|
-
|
|
630
|
+
suiteCases.reduce((n, c) => n + caseReps(c, parsed.runs, parsed.hot), 0);
|
|
631
|
+
const worstCaseMs = selected.length *
|
|
632
|
+
suiteCases.reduce((n, c) => {
|
|
633
|
+
const pin = pinnedNumPredict(c, parsed.throughputNumPredict);
|
|
634
|
+
return n + caseReps(c, parsed.runs, parsed.hot) * attemptTimeoutMs(pin);
|
|
635
|
+
}, 0);
|
|
592
636
|
if (!parsed.json) {
|
|
593
637
|
const skipBits = [
|
|
594
638
|
skipped.filter((s) => s.reason === "non-completion").length
|
|
@@ -603,7 +647,10 @@ export async function main() {
|
|
|
603
647
|
` × suite=${parsed.suite} × runs=${parsed.runs}` +
|
|
604
648
|
(parsed.hot ? " (hot: +1 discarded)" : ""));
|
|
605
649
|
console.error(`Cases: ~${caseCount} chat calls timeout: ${BENCH_TIMEOUT_MS}ms/case` +
|
|
606
|
-
|
|
650
|
+
(parsed.suite === "full"
|
|
651
|
+
? ` (long case ≥${attemptTimeoutMs(LONG_THROUGHPUT_NUM_PREDICT)}ms)`
|
|
652
|
+
: "") +
|
|
653
|
+
` worst-case ceiling: ~${Math.ceil(worstCaseMs / 60000)}m if every call times out`);
|
|
607
654
|
if (!parsed.hot && !isLoopback(host) && (selected.length >= 2 || parsed.coldLoad)) {
|
|
608
655
|
const bits = [
|
|
609
656
|
selected.length >= 2 ? "unloads each model before the next" : "",
|
|
@@ -665,8 +712,8 @@ export async function main() {
|
|
|
665
712
|
let modelError;
|
|
666
713
|
for (const caseDef of suiteCases) {
|
|
667
714
|
const attempts = [];
|
|
668
|
-
const discardFirst = parsed.hot
|
|
669
|
-
const reps = caseDef
|
|
715
|
+
const discardFirst = discardFirstThroughput(caseDef, parsed.hot);
|
|
716
|
+
const reps = caseReps(caseDef, parsed.runs, parsed.hot);
|
|
670
717
|
for (let i = 0; i < reps; i += 1) {
|
|
671
718
|
const discarded = discardFirst && i === 0;
|
|
672
719
|
const attempt = await runCaseAttempt({
|
|
@@ -758,6 +805,7 @@ export async function main() {
|
|
|
758
805
|
temperature: 0,
|
|
759
806
|
seed: 0,
|
|
760
807
|
throughput_num_predict: parsed.throughputNumPredict,
|
|
808
|
+
throughput_long_num_predict: parsed.suite === "full" ? LONG_THROUGHPUT_NUM_PREDICT : null,
|
|
761
809
|
num_ctx: baseSettings.num_ctx ?? null,
|
|
762
810
|
warmup: parsed.warmup,
|
|
763
811
|
hot: parsed.hot,
|
package/dist/mcp.js
CHANGED
|
@@ -200,7 +200,7 @@ const TOOLS = [
|
|
|
200
200
|
{
|
|
201
201
|
name: "ollanet_ps",
|
|
202
202
|
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
|
|
203
|
+
"scan is on-disk inventory; ps is what is resident right now, with CPU/GPU %.",
|
|
204
204
|
inputSchema: {
|
|
205
205
|
type: "object",
|
|
206
206
|
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;
|
package/dist/ps.d.ts
CHANGED
|
@@ -8,7 +8,12 @@ import { type AppConfig } from "./config.js";
|
|
|
8
8
|
export declare function helpText(): string;
|
|
9
9
|
export interface LoadedModel {
|
|
10
10
|
name: string;
|
|
11
|
+
size?: number;
|
|
11
12
|
size_vram?: number;
|
|
13
|
+
cpu_percent?: number;
|
|
14
|
+
gpu_percent?: number;
|
|
15
|
+
/** Same wording as `ollama ps` PROCESSOR (e.g. "100% GPU", "83%/17% CPU/GPU"). */
|
|
16
|
+
processor?: string;
|
|
12
17
|
context_length?: number;
|
|
13
18
|
digest?: string;
|
|
14
19
|
tuned: boolean;
|
|
@@ -28,5 +33,11 @@ export interface PsOptions {
|
|
|
28
33
|
export interface PsResult {
|
|
29
34
|
hosts: LoadedHost[];
|
|
30
35
|
}
|
|
36
|
+
/** Match `ollama ps` PROCESSOR: CPU% from (size - size_vram) / size. */
|
|
37
|
+
export declare function processorSplit(size?: number, sizeVram?: number): {
|
|
38
|
+
cpu_percent?: number;
|
|
39
|
+
gpu_percent?: number;
|
|
40
|
+
processor?: string;
|
|
41
|
+
};
|
|
31
42
|
export declare function listLoaded(opts?: PsOptions): Promise<PsResult>;
|
|
32
43
|
export declare function main(): Promise<void>;
|
package/dist/ps.js
CHANGED
|
@@ -23,7 +23,8 @@ Examples:
|
|
|
23
23
|
ollanet ps studio
|
|
24
24
|
ollanet ps studio --json
|
|
25
25
|
|
|
26
|
-
scan lists models on disk. ps lists what is loaded
|
|
26
|
+
scan lists models on disk. ps lists what is loaded and the CPU/GPU split
|
|
27
|
+
(same rule as ollama ps: size_vram / size).
|
|
27
28
|
|
|
28
29
|
Options:
|
|
29
30
|
--machine <name> One host (default: every discovered host)
|
|
@@ -76,11 +77,31 @@ function formatBytes(bytes) {
|
|
|
76
77
|
}
|
|
77
78
|
return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
|
|
78
79
|
}
|
|
80
|
+
/** Match `ollama ps` PROCESSOR: CPU% from (size - size_vram) / size. */
|
|
81
|
+
export function processorSplit(size, sizeVram) {
|
|
82
|
+
const total = size ?? 0;
|
|
83
|
+
const vram = sizeVram ?? 0;
|
|
84
|
+
if (vram === 0 && total === 0)
|
|
85
|
+
return {};
|
|
86
|
+
if (vram === 0)
|
|
87
|
+
return { cpu_percent: 100, gpu_percent: 0, processor: "100% CPU" };
|
|
88
|
+
if (total > 0 && vram === total) {
|
|
89
|
+
return { cpu_percent: 0, gpu_percent: 100, processor: "100% GPU" };
|
|
90
|
+
}
|
|
91
|
+
if (vram > total || total === 0)
|
|
92
|
+
return { processor: "Unknown" };
|
|
93
|
+
const cpu = Math.round(((total - vram) / total) * 100);
|
|
94
|
+
const gpu = 100 - cpu;
|
|
95
|
+
return { cpu_percent: cpu, gpu_percent: gpu, processor: `${cpu}%/${gpu}% CPU/GPU` };
|
|
96
|
+
}
|
|
79
97
|
function toLoaded(m) {
|
|
80
98
|
const name = (m.name ?? m.model ?? "").trim();
|
|
99
|
+
const split = processorSplit(m.size, m.size_vram);
|
|
81
100
|
return {
|
|
82
101
|
name,
|
|
102
|
+
size: m.size,
|
|
83
103
|
size_vram: m.size_vram,
|
|
104
|
+
...split,
|
|
84
105
|
context_length: m.context_length,
|
|
85
106
|
digest: m.digest,
|
|
86
107
|
tuned: looksTuned(name),
|
|
@@ -143,7 +164,7 @@ function printPs(result) {
|
|
|
143
164
|
const vram = formatBytes(m.size_vram);
|
|
144
165
|
const ctx = m.context_length != null ? `ctx ${m.context_length}` : "";
|
|
145
166
|
const tag = m.tuned ? " [tuned]" : "";
|
|
146
|
-
const meta = [vram ? `VRAM ${vram}` : "", ctx].filter(Boolean).join(" ");
|
|
167
|
+
const meta = [vram ? `VRAM ${vram}` : "", m.processor, ctx].filter(Boolean).join(" ");
|
|
147
168
|
process.stdout.write(meta ? ` ${m.name} ${meta}${tag}\n` : ` ${m.name}${tag}\n`);
|
|
148
169
|
}
|
|
149
170
|
}
|