@otto-code/brain 0.8.7 → 0.8.8
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/dist/cli.js +2 -1
- package/dist/commands/catalog.d.ts +2 -0
- package/dist/commands/catalog.js +1 -0
- package/dist/commands/pull.d.ts +1 -0
- package/dist/commands/pull.js +55 -3
- package/dist/commands/runtime.d.ts +3 -0
- package/dist/commands/runtime.js +65 -18
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.js +36 -7
- package/dist/config/profile-edit.d.ts +1 -1
- package/dist/config/profile-edit.js +40 -10
- package/dist/config/profiles.js +43 -3
- package/dist/config/schema.d.ts +528 -0
- package/dist/config/schema.js +24 -0
- package/dist/config/store.js +16 -14
- package/dist/models/download.d.ts +7 -0
- package/dist/models/download.js +160 -17
- package/dist/models/enrich.d.ts +0 -19
- package/dist/models/enrich.js +89 -3
- package/dist/models/hf.d.ts +14 -1
- package/dist/models/hf.js +239 -6
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.js +8 -5
- package/dist/models/manage.d.ts +4 -0
- package/dist/models/manage.js +34 -4
- package/dist/models/scan.js +8 -42
- package/dist/runtime/args.js +5 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/managed.d.ts +40 -0
- package/dist/runtime/managed.js +146 -7
- package/dist/service/host-api.d.ts +3 -2
- package/dist/service/host-api.js +118 -8
- package/dist/service/serve.js +39 -12
- package/dist/types.d.ts +20 -0
- package/dist/vram.d.ts +3 -0
- package/dist/vram.js +21 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { addRestartOptions, addServeOptions, addStartOptions, addStatusOptions,
|
|
|
15
15
|
import { addPullOptions, runPullCommand } from "./commands/pull.js";
|
|
16
16
|
import { addReportOptions, runReportCommand } from "./commands/report.js";
|
|
17
17
|
import { addRescoreOptions, runRescoreCommand } from "./commands/rescore.js";
|
|
18
|
-
import { addRuntimeInstallOptions, addRuntimeListOptions, runRuntimeInstallCommand, runRuntimeListCommand, } from "./commands/runtime.js";
|
|
18
|
+
import { addRuntimeInstallOptions, addRuntimeListOptions, addRuntimeRemoveOptions, runRuntimeInstallCommand, runRuntimeListCommand, runRuntimeRemoveCommand, } from "./commands/runtime.js";
|
|
19
19
|
import { addScanOptions, runScanCommand } from "./commands/scan.js";
|
|
20
20
|
import { addAddOptions, addSearchOptions, runAddCommand, runSearchCommand, } from "./commands/search.js";
|
|
21
21
|
import { addSweepOptions, runSweepCommand } from "./commands/sweep.js";
|
|
@@ -57,6 +57,7 @@ export function registerBrainCommands(program) {
|
|
|
57
57
|
const runtime = program.command("runtime").description("Manage the llama.cpp runtime");
|
|
58
58
|
addRuntimeListOptions(runtime.command("list")).action(withOutput(runRuntimeListCommand));
|
|
59
59
|
addRuntimeInstallOptions(runtime.command("install")).action(withOutput(runRuntimeInstallCommand));
|
|
60
|
+
addRuntimeRemoveOptions(runtime.command("remove")).action(withOutput(runRuntimeRemoveCommand));
|
|
60
61
|
// config subgroup.
|
|
61
62
|
const config = program.command("config").description("Inspect and edit brain config");
|
|
62
63
|
addConfigShowOptions(config.command("show")).action(withOutput(runConfigShowCommand));
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* params, tier, use-cases) that only the JSON output surfaces.
|
|
9
9
|
*/
|
|
10
10
|
import type { Command } from "commander";
|
|
11
|
+
import type { CatalogModel } from "../config/schema.js";
|
|
11
12
|
import type { AnyCommandResult, OutputSchema } from "../output/index.js";
|
|
12
13
|
export interface CatalogRow {
|
|
13
14
|
id: string;
|
|
@@ -25,6 +26,7 @@ export interface CatalogRow {
|
|
|
25
26
|
tier: string;
|
|
26
27
|
useCases: string[];
|
|
27
28
|
why: string;
|
|
29
|
+
components?: CatalogModel["components"];
|
|
28
30
|
}
|
|
29
31
|
export declare const catalogSchema: OutputSchema<CatalogRow>;
|
|
30
32
|
export declare function addCatalogOptions(cmd: Command): Command;
|
package/dist/commands/catalog.js
CHANGED
package/dist/commands/pull.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface PullOptionsInput {
|
|
|
17
17
|
file?: string;
|
|
18
18
|
quant?: string;
|
|
19
19
|
listQuants?: boolean;
|
|
20
|
+
component?: string[];
|
|
20
21
|
}
|
|
21
22
|
export declare function runPullCommand(modelArg: string, options: PullOptionsInput, _command: Command): Promise<AnyCommandResult<PullRow>>;
|
|
22
23
|
//# sourceMappingURL=pull.d.ts.map
|
package/dist/commands/pull.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadBrainConfig, loadCatalog } from "../config/index.js";
|
|
2
|
-
import { downloadRepoFiles, listRepoQuants, managedModelsDir, pullModel, resolveHfToken, } from "../models/index.js";
|
|
2
|
+
import { downloadRepoFiles, bundleDownloadPlan, listRepoQuants, managedModelsDir, pullModel, resolveHfToken, } from "../models/index.js";
|
|
3
3
|
import { CommandError } from "../output/types.js";
|
|
4
4
|
import { formatBytes } from "../models/scan.js";
|
|
5
5
|
import { withActivity } from "../service/activity.js";
|
|
@@ -44,6 +44,7 @@ export function addPullOptions(cmd) {
|
|
|
44
44
|
.argument("<model>", "catalog id or name fragment")
|
|
45
45
|
.option("--file <name.gguf>", "explicit GGUF file name in the repo")
|
|
46
46
|
.option("--quant <label>", "download a specific quantization (e.g. Q5_K_M)")
|
|
47
|
+
.option("--component <id...>", "download optional bundle component ids")
|
|
47
48
|
.option("--list-quants", "list the quantizations the repo offers and exit");
|
|
48
49
|
}
|
|
49
50
|
export async function runPullCommand(modelArg, options, _command) {
|
|
@@ -51,6 +52,56 @@ export async function runPullCommand(modelArg, options, _command) {
|
|
|
51
52
|
const catalog = loadCatalog();
|
|
52
53
|
const model = findCatalogModel(catalog.models, modelArg);
|
|
53
54
|
const token = resolveHfToken(config);
|
|
55
|
+
// A bundle combines one selected primary quant and explicit optional
|
|
56
|
+
// companions. It must not pull the generic discovery projector as a side
|
|
57
|
+
// effect, because that makes the download plan ambiguous.
|
|
58
|
+
if (model.components && !options.listQuants && !options.file) {
|
|
59
|
+
const { quants } = await listRepoQuants(model.hfRepo, token);
|
|
60
|
+
const choice = options.quant
|
|
61
|
+
? quants.find((quant) => quant.quant.toLowerCase() === options.quant.toLowerCase())
|
|
62
|
+
: undefined;
|
|
63
|
+
if (options.quant && !choice) {
|
|
64
|
+
throw new CommandError({
|
|
65
|
+
code: "NO_QUANT",
|
|
66
|
+
message: `${model.hfRepo} has no ${options.quant}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const plan = bundleDownloadPlan(model, options.component ?? [], choice?.files, choice?.sizeBytes);
|
|
70
|
+
let lastPct = -1;
|
|
71
|
+
const written = await withActivity("download", { target: model.name }, (activity) => downloadRepoFiles({
|
|
72
|
+
repo: plan.repo,
|
|
73
|
+
files: plan.files,
|
|
74
|
+
destRoot: managedModelsDir(config),
|
|
75
|
+
token,
|
|
76
|
+
onProgress: (progress) => {
|
|
77
|
+
activity.update(plan.totalBytes ? progress.receivedBytes / plan.totalBytes : null);
|
|
78
|
+
// The daemon owns the UI job record and only sees this child's
|
|
79
|
+
// stdout/stderr. Activity also feeds the host status, but emitting
|
|
80
|
+
// here is what keeps the Library's bundle progress ring live.
|
|
81
|
+
const pct = plan.totalBytes
|
|
82
|
+
? Math.floor((progress.receivedBytes / plan.totalBytes) * 100)
|
|
83
|
+
: 0;
|
|
84
|
+
// Network chunks do not land exactly on 5% boundaries. Emit every
|
|
85
|
+
// new integer so the daemon receives continuous progress instead of
|
|
86
|
+
// often seeing only the initial and final updates.
|
|
87
|
+
if (pct > lastPct) {
|
|
88
|
+
lastPct = pct;
|
|
89
|
+
process.stderr.write(` ${model.name}${choice ? ` ${choice.quant}` : ""}: ${pct}%\r`);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
process.stderr.write("\n");
|
|
94
|
+
return {
|
|
95
|
+
type: "single",
|
|
96
|
+
data: {
|
|
97
|
+
model: choice ? `${model.name} (${choice.quant})` : model.name,
|
|
98
|
+
repo: model.hfRepo,
|
|
99
|
+
path: written[0] ?? "(already present)",
|
|
100
|
+
size: plan.totalBytes ? formatBytes(plan.totalBytes) : "-",
|
|
101
|
+
},
|
|
102
|
+
schema: pullSchema,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
54
105
|
// Discover-and-choose paths both need the repo's quant listing.
|
|
55
106
|
if (options.listQuants || options.quant) {
|
|
56
107
|
const { quants, mmproj } = await listRepoQuants(model.hfRepo, token);
|
|
@@ -61,6 +112,7 @@ export async function runPullCommand(modelArg, options, _command) {
|
|
|
61
112
|
quant: q.quant,
|
|
62
113
|
size: formatBytes(q.sizeBytes),
|
|
63
114
|
files: q.files.length,
|
|
115
|
+
fileNames: q.files,
|
|
64
116
|
})),
|
|
65
117
|
schema: quantSchema,
|
|
66
118
|
};
|
|
@@ -87,7 +139,7 @@ export async function runPullCommand(modelArg, options, _command) {
|
|
|
87
139
|
onProgress: (p) => {
|
|
88
140
|
activity.update(total ? p.receivedBytes / total : null);
|
|
89
141
|
const pct = total ? Math.floor((p.receivedBytes / total) * 100) : 0;
|
|
90
|
-
if (pct
|
|
142
|
+
if (pct > lastPct) {
|
|
91
143
|
lastPct = pct;
|
|
92
144
|
process.stderr.write(` ${model.name} ${choice.quant}: ${pct}%\r`);
|
|
93
145
|
}
|
|
@@ -119,7 +171,7 @@ export async function runPullCommand(modelArg, options, _command) {
|
|
|
119
171
|
return;
|
|
120
172
|
activity.update(p.receivedBytes / p.totalBytes);
|
|
121
173
|
const pct = Math.floor((p.receivedBytes / p.totalBytes) * 100);
|
|
122
|
-
if (pct
|
|
174
|
+
if (pct > lastPct) {
|
|
123
175
|
lastPct = pct;
|
|
124
176
|
process.stderr.write(` ${model.name}: ${pct}%\r`);
|
|
125
177
|
}
|
|
@@ -8,6 +8,7 @@ import type { Command } from "commander";
|
|
|
8
8
|
import type { AnyCommandResult } from "../output/index.js";
|
|
9
9
|
export interface RuntimeRow {
|
|
10
10
|
label: string;
|
|
11
|
+
displayName: string;
|
|
11
12
|
version: string;
|
|
12
13
|
source: string;
|
|
13
14
|
dir: string;
|
|
@@ -15,6 +16,8 @@ export interface RuntimeRow {
|
|
|
15
16
|
export declare function addRuntimeListOptions(cmd: Command): Command;
|
|
16
17
|
export declare function runRuntimeListCommand(_options: unknown, _command: Command): Promise<AnyCommandResult<RuntimeRow>>;
|
|
17
18
|
export declare function addRuntimeInstallOptions(cmd: Command): Command;
|
|
19
|
+
export declare function addRuntimeRemoveOptions(cmd: Command): Command;
|
|
20
|
+
export declare function runRuntimeRemoveCommand(name: string, _options: unknown, _command: Command): Promise<AnyCommandResult<RuntimeRow>>;
|
|
18
21
|
export declare function runRuntimeInstallCommand(options: {
|
|
19
22
|
build?: string;
|
|
20
23
|
variant?: string;
|
package/dist/commands/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { loadBrainConfig } from "../config/index.js";
|
|
2
2
|
import { resolveBrainPaths } from "../config/paths.js";
|
|
3
3
|
import { CommandError } from "../output/types.js";
|
|
4
|
-
import { defaultRuntimeSpec, installManagedRuntime, listAllRuntimes, listRuntimeDevices, probeNvidiaGpu, supportedVariants, } from "../runtime/index.js";
|
|
4
|
+
import { DEFAULT_LLAMA_BUILD, defaultRuntimeSpec, installManagedRuntime, MissingAssetError, removeManagedRuntime, resolveLatestBuildOrPin, listAllRuntimes, listRuntimeDevices, probeNvidiaGpu, supportedVariants, } from "../runtime/index.js";
|
|
5
5
|
const runtimeSchema = {
|
|
6
6
|
idField: "dir",
|
|
7
7
|
columns: [
|
|
@@ -19,6 +19,7 @@ const runtimeSchema = {
|
|
|
19
19
|
function toRows() {
|
|
20
20
|
return listAllRuntimes().map((r) => ({
|
|
21
21
|
label: r.label,
|
|
22
|
+
displayName: r.displayName ?? `${r.label} · ${r.version}`,
|
|
22
23
|
version: r.version,
|
|
23
24
|
source: r.source,
|
|
24
25
|
dir: r.dir,
|
|
@@ -33,9 +34,40 @@ export async function runRuntimeListCommand(_options, _command) {
|
|
|
33
34
|
export function addRuntimeInstallOptions(cmd) {
|
|
34
35
|
return cmd
|
|
35
36
|
.description("Download a self-contained llama.cpp runtime")
|
|
36
|
-
.option("--build <tag>", "llama.cpp release build tag")
|
|
37
|
+
.option("--build <tag>", "llama.cpp release build tag (for example b10355)")
|
|
37
38
|
.option("--variant <name>", `accelerator to install (${supportedVariants().join("|")}); defaults to the best one this machine can use`);
|
|
38
39
|
}
|
|
40
|
+
export function addRuntimeRemoveOptions(cmd) {
|
|
41
|
+
return cmd.description("Remove one Otto-managed runtime").argument("<name>");
|
|
42
|
+
}
|
|
43
|
+
export async function runRuntimeRemoveCommand(name, _options, _command) {
|
|
44
|
+
loadBrainConfig();
|
|
45
|
+
removeManagedRuntime(resolveBrainPaths().runtimesDir, name);
|
|
46
|
+
return { type: "list", data: toRows(), schema: runtimeSchema };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Every warning this command emits is one line on purpose. The daemon's
|
|
50
|
+
* BrainOpsManager keeps the *last* stderr line as the job's message, so a
|
|
51
|
+
* multi-line warning would surface in the UI as a dangling fragment. As one line
|
|
52
|
+
* it survives intact on both the CLI and the GUI.
|
|
53
|
+
*/
|
|
54
|
+
function warn(message) {
|
|
55
|
+
process.stderr.write(` warning: ${message.replace(/\s+/gu, " ").trim()}\n`);
|
|
56
|
+
}
|
|
57
|
+
/** Download + extract a spec, reporting percentage and phase on stderr. */
|
|
58
|
+
async function install(spec, runtimesDir) {
|
|
59
|
+
process.stderr.write(` installing ${spec.label} (${spec.version})…\n`);
|
|
60
|
+
const runtime = await installManagedRuntime(spec, runtimesDir, (p) => {
|
|
61
|
+
if (p.phase === "downloading" && p.totalBytes) {
|
|
62
|
+
const pct = Math.floor(((p.receivedBytes ?? 0) / p.totalBytes) * 100);
|
|
63
|
+
process.stderr.write(` ${pct}%\r`);
|
|
64
|
+
}
|
|
65
|
+
if (p.phase === "extracting")
|
|
66
|
+
process.stderr.write("\n extracting…\n");
|
|
67
|
+
});
|
|
68
|
+
process.stderr.write("\n");
|
|
69
|
+
return runtime;
|
|
70
|
+
}
|
|
39
71
|
export async function runRuntimeInstallCommand(options, _command) {
|
|
40
72
|
loadBrainConfig();
|
|
41
73
|
const { runtimesDir } = resolveBrainPaths();
|
|
@@ -49,38 +81,53 @@ export async function runRuntimeInstallCommand(options, _command) {
|
|
|
49
81
|
}
|
|
50
82
|
// Only "auto" needs the GPU probe, and only to choose between CUDA and Vulkan
|
|
51
83
|
// on the platforms that have both.
|
|
52
|
-
const
|
|
84
|
+
const target = {
|
|
53
85
|
variant: options.variant ?? "auto",
|
|
54
86
|
hasNvidiaGpu: options.variant ? undefined : await probeNvidiaGpu(),
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
};
|
|
88
|
+
// "latest" is a request to update, not a promise upstream can always keep: the
|
|
89
|
+
// lookup is unauthenticated and rate limited, so it falls back to the pin.
|
|
90
|
+
const resolvedLatest = options.build === "latest" ? await resolveLatestBuildOrPin() : null;
|
|
91
|
+
if (resolvedLatest?.warning)
|
|
92
|
+
warn(resolvedLatest.warning);
|
|
93
|
+
let spec = defaultRuntimeSpec(resolvedLatest ? resolvedLatest.build : options.build, target);
|
|
94
|
+
let runtime;
|
|
95
|
+
try {
|
|
96
|
+
runtime = await install(spec, runtimesDir);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// Asset names are not derivable from the build tag, and the scheme has
|
|
100
|
+
// already changed once (b4600 shipped `win-cuda-cu12.4-x64`, b10265 ships
|
|
101
|
+
// `win-cuda-12.4-x64`), so a tag resolved at run time can name assets that
|
|
102
|
+
// do not exist under it. The pin's names are the ones managed.test.ts holds,
|
|
103
|
+
// which makes it the only build this can fall back to. An explicit --build
|
|
104
|
+
// still fails loudly: the user named that tag on purpose.
|
|
105
|
+
if (!(error instanceof MissingAssetError) ||
|
|
106
|
+
!resolvedLatest ||
|
|
107
|
+
spec.version === DEFAULT_LLAMA_BUILD) {
|
|
108
|
+
throw error;
|
|
61
109
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
110
|
+
warn(`llama.cpp build ${spec.version} does not publish the asset this platform needs,` +
|
|
111
|
+
` so Otto installed the pinned build ${DEFAULT_LLAMA_BUILD} instead.`);
|
|
112
|
+
spec = defaultRuntimeSpec(DEFAULT_LLAMA_BUILD, target);
|
|
113
|
+
runtime = await install(spec, runtimesDir);
|
|
114
|
+
}
|
|
66
115
|
// A GPU variant that finds no device still runs - on the CPU, at roughly a
|
|
67
116
|
// fortieth of the prefill throughput, and says nothing about it. Measured on
|
|
68
117
|
// WSL2, which carries no NVIDIA Vulkan ICD. Warn rather than fail: the runtime
|
|
69
118
|
// is genuinely installed and usable, just not accelerated.
|
|
70
|
-
// One line on purpose. The daemon's BrainOpsManager keeps the *last* stderr line
|
|
71
|
-
// as the job's message, so a multi-line warning would surface in the UI as a
|
|
72
|
-
// dangling fragment. As one line it survives intact on both the CLI and the GUI.
|
|
73
119
|
if (spec.variant !== "cpu") {
|
|
74
120
|
const devices = await listRuntimeDevices(runtime);
|
|
75
121
|
if (devices.length === 0) {
|
|
76
|
-
|
|
77
|
-
` on the CPU until a working ${spec.variant} driver for this GPU is installed
|
|
122
|
+
warn(`this ${spec.variant} runtime reports no GPU device, so inference will run` +
|
|
123
|
+
` on the CPU until a working ${spec.variant} driver for this GPU is installed.`);
|
|
78
124
|
}
|
|
79
125
|
}
|
|
80
126
|
return {
|
|
81
127
|
type: "single",
|
|
82
128
|
data: {
|
|
83
129
|
label: runtime.label,
|
|
130
|
+
displayName: runtime.displayName ?? `${runtime.label} · ${runtime.version}`,
|
|
84
131
|
version: runtime.version,
|
|
85
132
|
source: runtime.source,
|
|
86
133
|
dir: runtime.dir,
|
|
@@ -12,6 +12,7 @@ export interface SearchRow {
|
|
|
12
12
|
likes: number;
|
|
13
13
|
gated: string;
|
|
14
14
|
installed: boolean;
|
|
15
|
+
summary: string | null;
|
|
15
16
|
}
|
|
16
17
|
export declare function addSearchOptions(cmd: Command): Command;
|
|
17
18
|
export declare function runSearchCommand(query: string, options: {
|
|
@@ -27,5 +28,7 @@ export declare function addAddOptions(cmd: Command): Command;
|
|
|
27
28
|
export declare function runAddCommand(repo: string, options: {
|
|
28
29
|
quant?: string;
|
|
29
30
|
listQuants?: boolean;
|
|
31
|
+
component?: string[];
|
|
32
|
+
primaryOnly?: boolean;
|
|
30
33
|
}, _command: Command): Promise<AnyCommandResult<AddRow>>;
|
|
31
34
|
//# sourceMappingURL=search.d.ts.map
|
package/dist/commands/search.js
CHANGED
|
@@ -5,16 +5,19 @@ import { CommandError } from "../output/types.js";
|
|
|
5
5
|
/** What is already on disk, so search + quant listings can mark installed rows. */
|
|
6
6
|
function installedIndex(config) {
|
|
7
7
|
const repos = new Set();
|
|
8
|
-
const quants = new
|
|
8
|
+
const quants = new Map();
|
|
9
|
+
const projectorRepos = new Set();
|
|
9
10
|
for (const model of scanModels(config)) {
|
|
10
11
|
const repo = repoOfModel(model);
|
|
11
12
|
if (!repo)
|
|
12
13
|
continue;
|
|
13
14
|
repos.add(repo.toLowerCase());
|
|
14
15
|
if (model.quant)
|
|
15
|
-
quants.
|
|
16
|
+
quants.set(`${repo.toLowerCase()} ${model.quant.toUpperCase()}`, model.id);
|
|
17
|
+
if (model.mmprojPath)
|
|
18
|
+
projectorRepos.add(repo.toLowerCase());
|
|
16
19
|
}
|
|
17
|
-
return { repos, quants };
|
|
20
|
+
return { repos, quants, projectorRepos };
|
|
18
21
|
}
|
|
19
22
|
const searchSchema = {
|
|
20
23
|
idField: "repo",
|
|
@@ -46,6 +49,7 @@ export async function runSearchCommand(query, options, _command) {
|
|
|
46
49
|
likes: r.likes,
|
|
47
50
|
gated: r.gated ? "yes" : "",
|
|
48
51
|
installed: repos.has(r.repo.toLowerCase()),
|
|
52
|
+
summary: r.summary,
|
|
49
53
|
})),
|
|
50
54
|
schema: searchSchema,
|
|
51
55
|
};
|
|
@@ -73,6 +77,8 @@ export function addAddOptions(cmd) {
|
|
|
73
77
|
.description("Download a model from an arbitrary Hugging Face repo")
|
|
74
78
|
.argument("<repo>", "owner/repo on Hugging Face")
|
|
75
79
|
.option("--quant <label>", "quantization to download (e.g. Q5_K_M)")
|
|
80
|
+
.option("--component <id...>", "download optional discovered component ids")
|
|
81
|
+
.option("--primary-only", "download only the selected primary quant")
|
|
76
82
|
.option("--list-quants", "list the quantizations the repo offers and exit");
|
|
77
83
|
}
|
|
78
84
|
export async function runAddCommand(repo, options, _command) {
|
|
@@ -80,7 +86,7 @@ export async function runAddCommand(repo, options, _command) {
|
|
|
80
86
|
const token = resolveHfToken(config);
|
|
81
87
|
const { quants, mmproj } = await listRepoQuants(repo, token);
|
|
82
88
|
if (options.listQuants || !options.quant) {
|
|
83
|
-
const { quants: installedQuants } = installedIndex(config);
|
|
89
|
+
const { quants: installedQuants, projectorRepos } = installedIndex(config);
|
|
84
90
|
const listing = {
|
|
85
91
|
type: "list",
|
|
86
92
|
data: quants.map((q) => ({
|
|
@@ -88,7 +94,17 @@ export async function runAddCommand(repo, options, _command) {
|
|
|
88
94
|
size: formatBytes(q.sizeBytes),
|
|
89
95
|
sizeBytes: q.sizeBytes,
|
|
90
96
|
files: q.files.length,
|
|
97
|
+
modelId: installedQuants.get(`${repo.toLowerCase()} ${q.quant.toUpperCase()}`) ?? null,
|
|
91
98
|
installed: installedQuants.has(`${repo.toLowerCase()} ${q.quant.toUpperCase()}`),
|
|
99
|
+
...(mmproj
|
|
100
|
+
? {
|
|
101
|
+
projector: {
|
|
102
|
+
file: mmproj.files[0],
|
|
103
|
+
sizeBytes: mmproj.sizeBytes,
|
|
104
|
+
installed: projectorRepos.has(repo.toLowerCase()),
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
: {}),
|
|
92
108
|
})),
|
|
93
109
|
schema: addQuantSchema,
|
|
94
110
|
};
|
|
@@ -104,8 +120,18 @@ export async function runAddCommand(repo, options, _command) {
|
|
|
104
120
|
message: `${repo} has no ${options.quant} - available: ${quants.map((q) => q.quant).join(", ") || "none"}`,
|
|
105
121
|
});
|
|
106
122
|
}
|
|
107
|
-
const
|
|
108
|
-
const
|
|
123
|
+
const requested = new Set(options.component ?? []);
|
|
124
|
+
const unknown = [...requested].filter((id) => id !== "vision-projector");
|
|
125
|
+
if (unknown.length)
|
|
126
|
+
throw new CommandError({
|
|
127
|
+
code: "NO_COMPONENT",
|
|
128
|
+
message: `unknown bundle components: ${unknown.join(", ")}`,
|
|
129
|
+
});
|
|
130
|
+
const includeProjector = Boolean(mmproj) &&
|
|
131
|
+
(requested.has("vision-projector") ||
|
|
132
|
+
(!options.primaryOnly && options.component === undefined));
|
|
133
|
+
const files = [...choice.files, ...(includeProjector ? mmproj.files : [])];
|
|
134
|
+
const total = choice.sizeBytes + (includeProjector ? mmproj.sizeBytes : 0);
|
|
109
135
|
let lastPct = -1;
|
|
110
136
|
const written = await downloadRepoFiles({
|
|
111
137
|
repo,
|
|
@@ -114,7 +140,10 @@ export async function runAddCommand(repo, options, _command) {
|
|
|
114
140
|
token,
|
|
115
141
|
onProgress: (p) => {
|
|
116
142
|
const pct = total ? Math.floor((p.receivedBytes / total) * 100) : 0;
|
|
117
|
-
|
|
143
|
+
// A download chunk can jump straight over a five-percent boundary.
|
|
144
|
+
// Report each new integer percent so the UI ring never stalls until
|
|
145
|
+
// completion simply because no chunk hit an exact multiple of five.
|
|
146
|
+
if (pct > lastPct) {
|
|
118
147
|
lastPct = pct;
|
|
119
148
|
process.stderr.write(` ${repo} ${choice.quant}: ${pct}%\r`);
|
|
120
149
|
}
|
|
@@ -83,7 +83,7 @@ export interface SanitizeResult {
|
|
|
83
83
|
* Apply an editable patch to a profile, clamping every field to its range and
|
|
84
84
|
* dropping anything the model cannot use.
|
|
85
85
|
*
|
|
86
|
-
* Only the
|
|
86
|
+
* Only the supported editable keys are honoured. `modelPath`/`mmprojPath`/`modelId`
|
|
87
87
|
* are re-derived from the model on every read (`profiles.forModel`), so letting
|
|
88
88
|
* a caller set them would be a lie at best and a path-traversal at worst.
|
|
89
89
|
* `reasoningBudgetMessage`, `batchSize`, `ubatchSize` and `extraArgs` stay
|
|
@@ -54,8 +54,11 @@ export function nativeContextLimit(model) {
|
|
|
54
54
|
}
|
|
55
55
|
/** The editable fields, resolved against one model's capabilities. */
|
|
56
56
|
export function profileFieldDescriptors(model) {
|
|
57
|
-
const
|
|
58
|
-
|
|
57
|
+
const projector = model?.components?.find((component) => component.role === "vision_projector");
|
|
58
|
+
const hasProjector = model?.components
|
|
59
|
+
? Boolean(projector?.available)
|
|
60
|
+
: Boolean(model?.mmprojPath);
|
|
61
|
+
const fields = [
|
|
59
62
|
{
|
|
60
63
|
key: "contextSize",
|
|
61
64
|
label: "Context",
|
|
@@ -80,13 +83,6 @@ export function profileFieldDescriptors(model) {
|
|
|
80
83
|
available: true,
|
|
81
84
|
},
|
|
82
85
|
{ key: "flashAttention", label: "Flash attention", kind: "toggle", available: true },
|
|
83
|
-
{
|
|
84
|
-
key: "vision",
|
|
85
|
-
label: "Vision",
|
|
86
|
-
kind: "toggle",
|
|
87
|
-
available: hasProjector,
|
|
88
|
-
...(hasProjector ? {} : { unavailableReason: "no projector" }),
|
|
89
|
-
},
|
|
90
86
|
{
|
|
91
87
|
key: "reasoningBudget",
|
|
92
88
|
label: "Reasoning budget",
|
|
@@ -115,6 +111,23 @@ export function profileFieldDescriptors(model) {
|
|
|
115
111
|
available: true,
|
|
116
112
|
},
|
|
117
113
|
];
|
|
114
|
+
// Bundle models expose the projector in the component section below. Keep
|
|
115
|
+
// the legacy profile field for hand-scanned single-file models, but do not
|
|
116
|
+
// render two controls that write the same vision setting for bundles.
|
|
117
|
+
if (!model?.components) {
|
|
118
|
+
fields.splice(4, 0, {
|
|
119
|
+
key: "vision",
|
|
120
|
+
label: "Vision",
|
|
121
|
+
kind: "toggle",
|
|
122
|
+
available: hasProjector,
|
|
123
|
+
...(hasProjector
|
|
124
|
+
? {}
|
|
125
|
+
: {
|
|
126
|
+
unavailableReason: projector ? "download the vision component first" : "no projector",
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return fields;
|
|
118
131
|
}
|
|
119
132
|
/** Whether a KV cache type is quantised (anything that is not a float type). */
|
|
120
133
|
function isQuantised(cacheType) {
|
|
@@ -186,7 +199,7 @@ function clamp(value, min, max) {
|
|
|
186
199
|
* Apply an editable patch to a profile, clamping every field to its range and
|
|
187
200
|
* dropping anything the model cannot use.
|
|
188
201
|
*
|
|
189
|
-
* Only the
|
|
202
|
+
* Only the supported editable keys are honoured. `modelPath`/`mmprojPath`/`modelId`
|
|
190
203
|
* are re-derived from the model on every read (`profiles.forModel`), so letting
|
|
191
204
|
* a caller set them would be a lie at best and a path-traversal at worst.
|
|
192
205
|
* `reasoningBudgetMessage`, `batchSize`, `ubatchSize` and `extraArgs` stay
|
|
@@ -264,6 +277,23 @@ export function sanitizeProfilePatch(current, patch, model) {
|
|
|
264
277
|
next.vision = p.vision;
|
|
265
278
|
}
|
|
266
279
|
}
|
|
280
|
+
if ("enabledComponents" in p) {
|
|
281
|
+
if (!Array.isArray(p.enabledComponents) ||
|
|
282
|
+
!p.enabledComponents.every((id) => typeof id === "string")) {
|
|
283
|
+
throw new Error("enabledComponents must be an array of component ids");
|
|
284
|
+
}
|
|
285
|
+
const requested = [...new Set(p.enabledComponents)];
|
|
286
|
+
const available = new Set(model?.components
|
|
287
|
+
?.filter((component) => component.available)
|
|
288
|
+
.map((component) => component.id) ?? []);
|
|
289
|
+
const unavailable = requested.filter((id) => !available.has(id));
|
|
290
|
+
if (unavailable.length)
|
|
291
|
+
throw new Error(`components are not downloaded: ${unavailable.join(", ")}`);
|
|
292
|
+
next.enabledComponents = requested;
|
|
293
|
+
const projector = model?.components?.find((component) => component.role === "vision_projector" && requested.includes(component.id));
|
|
294
|
+
if (model?.components)
|
|
295
|
+
next.vision = Boolean(projector);
|
|
296
|
+
}
|
|
267
297
|
return { profile: next, adjustments };
|
|
268
298
|
}
|
|
269
299
|
//# sourceMappingURL=profile-edit.js.map
|
package/dist/config/profiles.js
CHANGED
|
@@ -2,6 +2,14 @@ import { DEFAULT_REASONING_MESSAGE, } from "./schema.js";
|
|
|
2
2
|
export function defaultProfile(model, defaults) {
|
|
3
3
|
const nativeContext = model?.metadata?.contextLength || 32768;
|
|
4
4
|
const contextCap = defaults?.contextCap ?? 225000;
|
|
5
|
+
const enabledComponents = model?.components
|
|
6
|
+
? model.components
|
|
7
|
+
.filter((component) => component.available && component.defaultLoad)
|
|
8
|
+
.map((component) => component.id)
|
|
9
|
+
: [];
|
|
10
|
+
const componentPaths = Object.fromEntries((model?.components ?? [])
|
|
11
|
+
.filter((component) => enabledComponents.includes(component.id) && component.path)
|
|
12
|
+
.map((component) => [component.role, component.path]));
|
|
5
13
|
return {
|
|
6
14
|
modelId: model?.id ?? null,
|
|
7
15
|
modelPath: model?.modelPath ?? null,
|
|
@@ -13,7 +21,13 @@ export function defaultProfile(model, defaults) {
|
|
|
13
21
|
cacheTypeV: defaults?.cacheTypeV ?? "q8_0",
|
|
14
22
|
flashAttention: defaults?.flashAttention ?? true, // required for a quantised V cache
|
|
15
23
|
gpuLayers: 999, // everything on GPU; the budget check guards this
|
|
16
|
-
vision
|
|
24
|
+
// Existing hand-scanned models keep the historical vision default. Bundles
|
|
25
|
+
// instead opt in only to installed components whose manifest says so.
|
|
26
|
+
enabledComponents,
|
|
27
|
+
componentPaths,
|
|
28
|
+
vision: model?.components
|
|
29
|
+
? model.components.some((component) => component.role === "vision_projector" && component.available && component.defaultLoad)
|
|
30
|
+
: Boolean(model?.mmprojPath),
|
|
17
31
|
reasoningBudget: defaults?.reasoningBudget ?? 1536,
|
|
18
32
|
reasoningBudgetMessage: DEFAULT_REASONING_MESSAGE,
|
|
19
33
|
parallelSlots: defaults?.parallelSlots ?? 1, // one agent at a time: max context per request
|
|
@@ -29,7 +43,27 @@ export function forModel(store, model, defaults) {
|
|
|
29
43
|
if (!stored)
|
|
30
44
|
return base;
|
|
31
45
|
// Paths are re-derived so a moved model library does not break the profile.
|
|
32
|
-
|
|
46
|
+
const enabledComponents = (stored.enabledComponents ?? []).filter((id) => model.components?.some((component) => component.id === id && component.available));
|
|
47
|
+
// COMPAT(bundleProfiles): added in v0.8.7, remove after 2027-02-11.
|
|
48
|
+
// Old vision profiles become the manifest's vision component when it exists.
|
|
49
|
+
if (model.components && enabledComponents.length === 0 && stored.vision) {
|
|
50
|
+
enabledComponents.push(...model.components
|
|
51
|
+
.filter((component) => component.role === "vision_projector" && component.available)
|
|
52
|
+
.map((component) => component.id));
|
|
53
|
+
}
|
|
54
|
+
const mmproj = model.components?.find((component) => component.role === "vision_projector" && enabledComponents.includes(component.id));
|
|
55
|
+
const componentPaths = Object.fromEntries((model.components ?? [])
|
|
56
|
+
.filter((component) => enabledComponents.includes(component.id) && component.path)
|
|
57
|
+
.map((component) => [component.role, component.path]));
|
|
58
|
+
return {
|
|
59
|
+
...base,
|
|
60
|
+
...stored,
|
|
61
|
+
enabledComponents,
|
|
62
|
+
modelPath: model.modelPath,
|
|
63
|
+
mmprojPath: mmproj?.path ?? (model.components ? null : model.mmprojPath),
|
|
64
|
+
componentPaths,
|
|
65
|
+
vision: model.components ? Boolean(mmproj) : stored.vision,
|
|
66
|
+
};
|
|
33
67
|
}
|
|
34
68
|
export function put(store, model, profile) {
|
|
35
69
|
store.profiles[model.id] = { ...profile, modelId: model.id };
|
|
@@ -37,7 +71,13 @@ export function put(store, model, profile) {
|
|
|
37
71
|
}
|
|
38
72
|
/** Calibration is keyed by cache types, since those change bytes/token. */
|
|
39
73
|
export function calibrationKey(profile) {
|
|
40
|
-
|
|
74
|
+
const components = [...(profile.enabledComponents ?? [])].sort();
|
|
75
|
+
// COMPAT(bundleCalibrationKey): added in v0.8.7, remove after 2027-02-11.
|
|
76
|
+
// A main-model-only load remains the historical identity; any enabled bundle
|
|
77
|
+
// artifact gets a distinct key and therefore cannot claim that measurement.
|
|
78
|
+
return components.length
|
|
79
|
+
? `${profile.cacheTypeK}:${profile.cacheTypeV}:components=${components.join(",")}`
|
|
80
|
+
: `${profile.cacheTypeK}:${profile.cacheTypeV}`;
|
|
41
81
|
}
|
|
42
82
|
/**
|
|
43
83
|
* True when this model has a stored calibration, but for different cache types
|