@otto-code/brain 0.8.6 → 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 +69 -13
- 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 +95 -4
- 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/models/hf.js
CHANGED
|
@@ -22,12 +22,229 @@ function authHeaders(token) {
|
|
|
22
22
|
function entryBytes(entry) {
|
|
23
23
|
return entry.size ?? entry.lfs?.size ?? 0;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Model-card summaries, and the three traps they set.
|
|
27
|
+
*
|
|
28
|
+
* 1. **`base_model` chains can be circular.** A quantizer's card names its base,
|
|
29
|
+
* whose card can name the quantization straight back. Two guards, because one
|
|
30
|
+
* is not enough: the chain of repos being resolved is carried down the
|
|
31
|
+
* recursion and re-entering a repo already on it is refused, and the chain is
|
|
32
|
+
* depth-capped. A guard on direct self-reference alone misses A -> B -> A.
|
|
33
|
+
* 2. **A shared cache of *pending* promises reintroduces the deadlock the chain
|
|
34
|
+
* guard just removed.** With three rows resolving concurrently, A -> B -> C -> A
|
|
35
|
+
* can have each stack awaiting another stack's in-flight entry, a wait-for
|
|
36
|
+
* cycle no per-stack chain can observe. So the cache holds *settled values
|
|
37
|
+
* only*, and the in-flight dedupe map is consulted **only at the top level**,
|
|
38
|
+
* never inside the recursion. Every in-flight promise therefore resolves
|
|
39
|
+
* without ever awaiting another in-flight promise, which makes the wait-for
|
|
40
|
+
* graph a forest and a cycle unrepresentable.
|
|
41
|
+
* 3. **The service is long lived.** The cache is capped and LRU-evicted, and
|
|
42
|
+
* entries expire, with a much shorter life for misses so a transient failure
|
|
43
|
+
* does not stick for the process lifetime.
|
|
44
|
+
*
|
|
45
|
+
* Every fetch also carries a timeout, and the caller's abort signal, so a slow or
|
|
46
|
+
* hanging remote cannot pin an RPC open.
|
|
47
|
+
*/
|
|
48
|
+
const SUMMARY_CACHE_MAX = 256;
|
|
49
|
+
const SUMMARY_TTL_MS = 30 * 60000;
|
|
50
|
+
/** Misses expire fast: a 404 today is often a card published tomorrow. */
|
|
51
|
+
const SUMMARY_MISS_TTL_MS = 5 * 60000;
|
|
52
|
+
const SUMMARY_FETCH_TIMEOUT_MS = 4000;
|
|
53
|
+
/** Deadline for the search and tree listings, which carry the actual payload. */
|
|
54
|
+
const REQUEST_TIMEOUT_MS = 15000;
|
|
55
|
+
/** How long a whole search may spend on summaries before degrading to null. */
|
|
56
|
+
const DEFAULT_SUMMARY_BUDGET_MS = 2500;
|
|
57
|
+
/** Repos one `base_model` branch may visit before giving up on finding prose. */
|
|
58
|
+
const MAX_BASE_MODEL_DEPTH = 3;
|
|
59
|
+
const cardSummaryCache = new Map();
|
|
60
|
+
const inFlightSummaries = new Map();
|
|
61
|
+
/** A live cache entry, or null when absent or expired. Refreshes LRU recency. */
|
|
62
|
+
function readCachedSummary(key) {
|
|
63
|
+
const hit = cardSummaryCache.get(key);
|
|
64
|
+
if (!hit)
|
|
65
|
+
return null;
|
|
66
|
+
if (hit.expiresAt <= Date.now()) {
|
|
67
|
+
cardSummaryCache.delete(key);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
// Re-insert so the most recently read key is the youngest in iteration order.
|
|
71
|
+
cardSummaryCache.delete(key);
|
|
72
|
+
cardSummaryCache.set(key, hit);
|
|
73
|
+
return hit;
|
|
74
|
+
}
|
|
75
|
+
function writeCachedSummary(key, value) {
|
|
76
|
+
cardSummaryCache.delete(key);
|
|
77
|
+
cardSummaryCache.set(key, {
|
|
78
|
+
value,
|
|
79
|
+
expiresAt: Date.now() + (value === null ? SUMMARY_MISS_TTL_MS : SUMMARY_TTL_MS),
|
|
80
|
+
});
|
|
81
|
+
// Map iterates in insertion order, so the first key is the least recently used.
|
|
82
|
+
while (cardSummaryCache.size > SUMMARY_CACHE_MAX) {
|
|
83
|
+
const oldest = cardSummaryCache.keys().next();
|
|
84
|
+
if (oldest.done)
|
|
85
|
+
break;
|
|
86
|
+
cardSummaryCache.delete(oldest.value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Drop every cached summary. Exported for tests and for a token change, which
|
|
90
|
+
* can flip which repos are readable at all. */
|
|
91
|
+
export function clearCardSummaryCache() {
|
|
92
|
+
cardSummaryCache.clear();
|
|
93
|
+
inFlightSummaries.clear();
|
|
94
|
+
}
|
|
95
|
+
function cleanCardSummary(markdown) {
|
|
96
|
+
const body = markdown.replace(/^---\s*[\s\S]*?---\s*/u, "");
|
|
97
|
+
const paragraphs = body
|
|
98
|
+
.split(/\r?\n\s*\r?\n/u)
|
|
99
|
+
.map((paragraph) => paragraph
|
|
100
|
+
.replace(/<[^>]+>/gu, " ")
|
|
101
|
+
.replace(/!?(\[[^\]]*\]\([^)]*\))/gu, "$1")
|
|
102
|
+
.replace(/[>*#`_]/gu, " ")
|
|
103
|
+
.replace(/\s+/gu, " ")
|
|
104
|
+
.trim())
|
|
105
|
+
.filter((paragraph) => paragraph.length >= 80);
|
|
106
|
+
const useful = paragraphs.find((paragraph) => !/^(community model|special thanks|disclaimers|model creator|gguf quantization)/iu.test(paragraph) &&
|
|
107
|
+
!/lm studio community models highlights program|lm studio is not the creator/iu.test(paragraph));
|
|
108
|
+
if (!useful)
|
|
109
|
+
return null;
|
|
110
|
+
const sentences = useful.match(/[^.!?]+[.!?]+(?:\s|$)/gu) ?? [useful];
|
|
111
|
+
const summary = sentences.slice(0, 2).join(" ").trim();
|
|
112
|
+
return summary.length >= 80 ? summary.slice(0, 360).trim() : null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A request the caller cannot proceed without, so a failure throws rather than
|
|
116
|
+
* reporting absence. It is still bounded: without a deadline a remote that
|
|
117
|
+
* accepts the connection and then goes quiet pins the RPC that awaits it open
|
|
118
|
+
* for as long as the socket lives, which is the whole reason the summary
|
|
119
|
+
* fetches carry one too.
|
|
120
|
+
*/
|
|
121
|
+
async function fetchHf(url, token, what) {
|
|
122
|
+
try {
|
|
123
|
+
return await fetch(url, {
|
|
124
|
+
headers: authHeaders(token),
|
|
125
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (cause) {
|
|
129
|
+
const timedOut = cause instanceof Error && cause.name === "TimeoutError";
|
|
130
|
+
throw new Error(timedOut
|
|
131
|
+
? `Hugging Face ${what} timed out after ${REQUEST_TIMEOUT_MS}ms`
|
|
132
|
+
: `Hugging Face ${what} could not reach the server`, { cause });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* One card fetch, bounded by its own timeout and by the caller's abort. Returns
|
|
137
|
+
* null on any failure: discovery reports absence rather than throwing, and a
|
|
138
|
+
* missing card is the overwhelmingly common case (most GGUF repos have none).
|
|
139
|
+
*/
|
|
140
|
+
async function fetchCard(url, token, signal) {
|
|
141
|
+
const timeout = AbortSignal.timeout(SUMMARY_FETCH_TIMEOUT_MS);
|
|
142
|
+
try {
|
|
143
|
+
return await fetch(url, {
|
|
144
|
+
headers: authHeaders(token),
|
|
145
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolve one repo's summary, following `base_model` when the repo has no card
|
|
154
|
+
* prose of its own. `chain` is every repo already being resolved on this branch,
|
|
155
|
+
* lowercased; it is both the cycle guard and the depth counter. Never consults
|
|
156
|
+
* the in-flight map (see the header note): the recursion only ever reads settled
|
|
157
|
+
* cache entries, so it cannot wait on another row's unfinished work.
|
|
158
|
+
*/
|
|
159
|
+
async function resolveCardSummary(repo, token, chain, signal) {
|
|
160
|
+
const key = repo.toLowerCase();
|
|
161
|
+
const cached = readCachedSummary(key);
|
|
162
|
+
if (cached)
|
|
163
|
+
return cached.value;
|
|
164
|
+
const readme = await fetchCard(`${HF_BASE}/${repo}/raw/main/README.md`, token, signal);
|
|
165
|
+
let summary = null;
|
|
166
|
+
if (readme?.ok) {
|
|
167
|
+
summary = cleanCardSummary(await readme.text().catch(() => ""));
|
|
168
|
+
}
|
|
169
|
+
// Only a depth cut makes the answer chain-specific: this repo might still
|
|
170
|
+
// have resolved from a shallower entry point, so that null must not be cached.
|
|
171
|
+
let truncated = false;
|
|
172
|
+
// Once the caller has given up, do not spend a second round trip chasing the
|
|
173
|
+
// base model: the answer can no longer be used.
|
|
174
|
+
if (summary === null && !signal?.aborted) {
|
|
175
|
+
if (chain.length >= MAX_BASE_MODEL_DEPTH) {
|
|
176
|
+
truncated = true;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// The repo names no prose of its own, so try the model it was quantized
|
|
180
|
+
// from. The `${HF_BASE}/` prefix is committed before the interpolation, so
|
|
181
|
+
// a hostile base_model cannot redirect this off huggingface.co and leak
|
|
182
|
+
// the token; keep that ordering if you touch these URLs.
|
|
183
|
+
const detail = await fetchCard(`${HF_BASE}/api/models/${repo}`, token, signal);
|
|
184
|
+
if (detail?.ok) {
|
|
185
|
+
const card = (await detail.json().catch(() => null));
|
|
186
|
+
const declared = card?.cardData?.base_model;
|
|
187
|
+
const baseModel = Array.isArray(declared) ? declared[0] : declared;
|
|
188
|
+
// Refusing a repo already on this branch is what breaks A -> B -> A.
|
|
189
|
+
// Caching the null is still correct here: a cycle carries no prose at
|
|
190
|
+
// any entry point, so every member resolves to null however it is
|
|
191
|
+
// reached.
|
|
192
|
+
const baseKey = baseModel?.toLowerCase();
|
|
193
|
+
if (baseModel && baseKey && !chain.includes(baseKey)) {
|
|
194
|
+
summary = await resolveCardSummary(baseModel, token, [...chain, baseKey], signal);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// An aborted branch resolved nothing because we stopped asking, not because
|
|
200
|
+
// the repo has no card. Caching that would let one slow search poison the next.
|
|
201
|
+
if (!truncated && !signal?.aborted)
|
|
202
|
+
writeCachedSummary(key, summary);
|
|
203
|
+
return summary;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Top-level entry: the only place the in-flight map is read, so duplicate repos
|
|
207
|
+
* in one result set share a fetch without ever forming a wait-for cycle.
|
|
208
|
+
*/
|
|
209
|
+
function modelCardSummary(repo, token, signal) {
|
|
210
|
+
const key = repo.toLowerCase();
|
|
211
|
+
const cached = readCachedSummary(key);
|
|
212
|
+
if (cached)
|
|
213
|
+
return Promise.resolve(cached.value);
|
|
214
|
+
const existing = inFlightSummaries.get(key);
|
|
215
|
+
if (existing)
|
|
216
|
+
return existing;
|
|
217
|
+
const pending = resolveCardSummary(repo, token, [key], signal)
|
|
218
|
+
.catch(() => null)
|
|
219
|
+
.finally(() => {
|
|
220
|
+
inFlightSummaries.delete(key);
|
|
221
|
+
});
|
|
222
|
+
inFlightSummaries.set(key, pending);
|
|
223
|
+
return pending;
|
|
224
|
+
}
|
|
225
|
+
async function mapWithConcurrency(values, limit, mapper) {
|
|
226
|
+
let cursor = 0;
|
|
227
|
+
await Promise.all(Array.from({ length: Math.min(limit, values.length) }, async () => {
|
|
228
|
+
while (cursor < values.length) {
|
|
229
|
+
const index = cursor++;
|
|
230
|
+
await mapper(values[index], index);
|
|
231
|
+
}
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
25
234
|
/**
|
|
26
235
|
* Search Hugging Face for GGUF model repos, most-downloaded first. Returns a
|
|
27
236
|
* normalized shape both the TUI and the Otto app can render; drill into a result
|
|
28
237
|
* with {@link listRepoQuants} to see and download its quantizations.
|
|
238
|
+
*
|
|
239
|
+
* Card summaries are a *bounded* enrichment, never a gate on the result set. Each
|
|
240
|
+
* one costs one or two extra round trips, so the whole batch shares one budget
|
|
241
|
+
* (`summaryBudgetMs`, 0 to skip them entirely); rows that miss it come back with
|
|
242
|
+
* `summary: null`, which every consumer already treats as "not available". When
|
|
243
|
+
* the budget expires the outstanding fetches are aborted rather than left to run,
|
|
244
|
+
* so the one-shot `otto brain search --json` the daemon shells out to can exit
|
|
245
|
+
* immediately instead of lingering on open sockets.
|
|
29
246
|
*/
|
|
30
|
-
export async function searchModels(query, { limit = 25, token = null } = {}) {
|
|
247
|
+
export async function searchModels(query, { limit = 25, token = null, summaryBudgetMs = DEFAULT_SUMMARY_BUDGET_MS, } = {}) {
|
|
31
248
|
const params = new URLSearchParams({
|
|
32
249
|
search: query,
|
|
33
250
|
filter: "gguf",
|
|
@@ -35,14 +252,12 @@ export async function searchModels(query, { limit = 25, token = null } = {}) {
|
|
|
35
252
|
direction: "-1",
|
|
36
253
|
limit: String(limit),
|
|
37
254
|
});
|
|
38
|
-
const res = await
|
|
39
|
-
headers: authHeaders(token),
|
|
40
|
-
});
|
|
255
|
+
const res = await fetchHf(`${HF_BASE}/api/models?${params.toString()}`, token, "search");
|
|
41
256
|
if (!res.ok) {
|
|
42
257
|
throw new Error(`Hugging Face search failed (${res.status}) for "${query}"`);
|
|
43
258
|
}
|
|
44
259
|
const entries = (await res.json());
|
|
45
|
-
|
|
260
|
+
const rows = entries.map((entry) => ({
|
|
46
261
|
repo: entry.id,
|
|
47
262
|
author: entry.author ?? entry.id.split("/")[0] ?? "",
|
|
48
263
|
downloads: entry.downloads ?? 0,
|
|
@@ -50,6 +265,24 @@ export async function searchModels(query, { limit = 25, token = null } = {}) {
|
|
|
50
265
|
updatedAt: entry.lastModified ?? null,
|
|
51
266
|
gated: Boolean(entry.gated),
|
|
52
267
|
}));
|
|
268
|
+
const summaries = rows.map(() => null);
|
|
269
|
+
if (summaryBudgetMs > 0 && rows.length > 0) {
|
|
270
|
+
const controller = new AbortController();
|
|
271
|
+
const gather = mapWithConcurrency(rows, 4, async (row, index) => {
|
|
272
|
+
summaries[index] = await modelCardSummary(row.repo, token, controller.signal);
|
|
273
|
+
}).catch(() => undefined);
|
|
274
|
+
let expire;
|
|
275
|
+
const deadline = new Promise((resolve) => {
|
|
276
|
+
expire = setTimeout(resolve, summaryBudgetMs);
|
|
277
|
+
// Never let the budget timer itself be the reason a CLI run stays alive.
|
|
278
|
+
expire.unref?.();
|
|
279
|
+
});
|
|
280
|
+
await Promise.race([gather, deadline]);
|
|
281
|
+
if (expire)
|
|
282
|
+
clearTimeout(expire);
|
|
283
|
+
controller.abort();
|
|
284
|
+
}
|
|
285
|
+
return rows.map((row, index) => ({ ...row, summary: summaries[index] ?? null }));
|
|
53
286
|
}
|
|
54
287
|
/**
|
|
55
288
|
* Quality order for display: higher bits-per-weight is more faithful and larger.
|
|
@@ -77,7 +310,7 @@ export function quantRank(quant) {
|
|
|
77
310
|
*/
|
|
78
311
|
export async function listRepoQuants(repo, token = null) {
|
|
79
312
|
const url = `${HF_BASE}/api/models/${repo}/tree/main?recursive=true`;
|
|
80
|
-
const res = await
|
|
313
|
+
const res = await fetchHf(url, token, `listing for ${repo}`);
|
|
81
314
|
if (!res.ok) {
|
|
82
315
|
throw new Error(`Hugging Face listing failed (${res.status}) for ${repo}`);
|
|
83
316
|
}
|
package/dist/models/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ import type { Model } from "../types.js";
|
|
|
3
3
|
export * from "./scan.js";
|
|
4
4
|
export { pickModel, pickAutoModel } from "./pick.js";
|
|
5
5
|
export { resolveModelsDirs, managedModelsDir, type ModelsDir } from "./dirs.js";
|
|
6
|
-
export { pullModel, downloadRepoFiles, type PullOptions, type PullProgress, type DownloadFilesOptions, } from "./download.js";
|
|
6
|
+
export { pullModel, bundleDownloadPlan, downloadRepoFiles, type PullOptions, type PullProgress, type DownloadFilesOptions, } from "./download.js";
|
|
7
7
|
export { matchCatalogEntry, enrichWithCatalog } from "./enrich.js";
|
|
8
|
-
export { resolveHfToken, listRepoQuants, searchModels, repoOfModel, quantRank, type QuantOption, type RepoQuants, type ModelSearchResult, } from "./hf.js";
|
|
8
|
+
export { resolveHfToken, listRepoQuants, searchModels, clearCardSummaryCache, repoOfModel, quantRank, type QuantOption, type RepoQuants, type ModelSearchResult, } from "./hf.js";
|
|
9
9
|
export { diskUsage, totalModelBytes, planDelete, deleteModelFiles, type DiskUsage, type DeletePlan, } from "./manage.js";
|
|
10
10
|
export interface ScanModelsOptions {
|
|
11
11
|
withMetadata?: boolean;
|
package/dist/models/index.js
CHANGED
|
@@ -9,14 +9,17 @@ import { scan } from "./scan.js";
|
|
|
9
9
|
export * from "./scan.js";
|
|
10
10
|
export { pickModel, pickAutoModel } from "./pick.js";
|
|
11
11
|
export { resolveModelsDirs, managedModelsDir } from "./dirs.js";
|
|
12
|
-
export { pullModel, downloadRepoFiles, } from "./download.js";
|
|
12
|
+
export { pullModel, bundleDownloadPlan, downloadRepoFiles, } from "./download.js";
|
|
13
13
|
export { matchCatalogEntry, enrichWithCatalog } from "./enrich.js";
|
|
14
|
-
export { resolveHfToken, listRepoQuants, searchModels, repoOfModel, quantRank, } from "./hf.js";
|
|
14
|
+
export { resolveHfToken, listRepoQuants, searchModels, clearCardSummaryCache, repoOfModel, quantRank, } from "./hf.js";
|
|
15
15
|
export { diskUsage, totalModelBytes, planDelete, deleteModelFiles, } from "./manage.js";
|
|
16
16
|
/** Scan every configured models directory (managed ∪ LM Studio), de-duplicated. */
|
|
17
17
|
export function scanModels(config, env = process.env, options = {}) {
|
|
18
18
|
const dirs = resolveModelsDirs(config, env);
|
|
19
|
-
|
|
19
|
+
// `Model.id` is the stable repo-relative artifact identity. The same file
|
|
20
|
+
// can exist in the managed store and LM Studio's store at different absolute
|
|
21
|
+
// paths; managed is scanned first and deliberately owns that collision.
|
|
22
|
+
const seenIds = new Set();
|
|
20
23
|
const all = [];
|
|
21
24
|
for (const { dir, origin } of dirs) {
|
|
22
25
|
for (const model of scan({
|
|
@@ -24,9 +27,9 @@ export function scanModels(config, env = process.env, options = {}) {
|
|
|
24
27
|
withMetadata: options.withMetadata ?? true,
|
|
25
28
|
origin,
|
|
26
29
|
})) {
|
|
27
|
-
if (
|
|
30
|
+
if (seenIds.has(model.id))
|
|
28
31
|
continue;
|
|
29
|
-
|
|
32
|
+
seenIds.add(model.id);
|
|
30
33
|
all.push(model);
|
|
31
34
|
}
|
|
32
35
|
}
|
package/dist/models/manage.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface DeletePlan {
|
|
|
15
15
|
files: string[];
|
|
16
16
|
bytes: number;
|
|
17
17
|
includesProjector: boolean;
|
|
18
|
+
componentIds: string[];
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Work out exactly which files deleting a model removes: its GGUF (all shards),
|
|
@@ -24,4 +25,7 @@ export interface DeletePlan {
|
|
|
24
25
|
export declare function planDelete(model: Model): DeletePlan;
|
|
25
26
|
/** Delete the files a {@link planDelete} chose, plus a now-empty repo directory. */
|
|
26
27
|
export declare function deleteModelFiles(model: Model): DeletePlan;
|
|
28
|
+
/** Remove one optional bundle artifact. The primary model is never accepted,
|
|
29
|
+
* and a loaded profile may not lose a component underneath llama.cpp. */
|
|
30
|
+
export declare function deleteComponentFile(model: Model, componentId: string): DeletePlan;
|
|
27
31
|
//# sourceMappingURL=manage.d.ts.map
|
package/dist/models/manage.js
CHANGED
|
@@ -66,7 +66,7 @@ export function planDelete(model) {
|
|
|
66
66
|
// The projector is shared across the repo's quants; only remove it if this was
|
|
67
67
|
// the last model GGUF in the directory.
|
|
68
68
|
let includesProjector = false;
|
|
69
|
-
if (model.mmprojPath) {
|
|
69
|
+
if (model.mmprojPath || model.components?.length) {
|
|
70
70
|
const dir = path.dirname(model.modelPath);
|
|
71
71
|
const doomed = new Set(files.map((f) => path.resolve(f)));
|
|
72
72
|
let othersRemain = false;
|
|
@@ -86,11 +86,24 @@ export function planDelete(model) {
|
|
|
86
86
|
othersRemain = true; // be conservative: keep the projector if unsure
|
|
87
87
|
}
|
|
88
88
|
if (!othersRemain) {
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
if (model.mmprojPath) {
|
|
90
|
+
add(model.mmprojPath);
|
|
91
|
+
includesProjector = true;
|
|
92
|
+
}
|
|
93
|
+
for (const component of model.components ?? []) {
|
|
94
|
+
if (component.path && !files.includes(component.path))
|
|
95
|
+
add(component.path);
|
|
96
|
+
}
|
|
91
97
|
}
|
|
92
98
|
}
|
|
93
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
files,
|
|
101
|
+
bytes,
|
|
102
|
+
includesProjector,
|
|
103
|
+
componentIds: (model.components ?? [])
|
|
104
|
+
.filter((component) => component.path && files.includes(component.path))
|
|
105
|
+
.map((component) => component.id),
|
|
106
|
+
};
|
|
94
107
|
}
|
|
95
108
|
/** Delete the files a {@link planDelete} chose, plus a now-empty repo directory. */
|
|
96
109
|
export function deleteModelFiles(model) {
|
|
@@ -109,4 +122,21 @@ export function deleteModelFiles(model) {
|
|
|
109
122
|
}
|
|
110
123
|
return plan;
|
|
111
124
|
}
|
|
125
|
+
/** Remove one optional bundle artifact. The primary model is never accepted,
|
|
126
|
+
* and a loaded profile may not lose a component underneath llama.cpp. */
|
|
127
|
+
export function deleteComponentFile(model, componentId) {
|
|
128
|
+
const component = model.components?.find((candidate) => candidate.id === componentId);
|
|
129
|
+
if (!component || !component.path)
|
|
130
|
+
throw new Error(`component "${componentId}" is not installed`);
|
|
131
|
+
if (component.required)
|
|
132
|
+
throw new Error("required bundle components cannot be removed separately");
|
|
133
|
+
const bytes = fs.statSync(component.path).size;
|
|
134
|
+
fs.rmSync(component.path, { force: true });
|
|
135
|
+
return {
|
|
136
|
+
files: [component.path],
|
|
137
|
+
bytes,
|
|
138
|
+
includesProjector: component.role === "vision_projector",
|
|
139
|
+
componentIds: [component.id],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
112
142
|
//# sourceMappingURL=manage.js.map
|
package/dist/models/scan.js
CHANGED
|
@@ -9,42 +9,11 @@ import os from "node:os";
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import * as gguf from "../gguf.js";
|
|
11
11
|
export const LMSTUDIO_MODELS_DIR = path.join(os.homedir(), ".lmstudio", "models");
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"IQ2_XXS",
|
|
18
|
-
"IQ2_XS",
|
|
19
|
-
"IQ2_S",
|
|
20
|
-
"IQ2_M",
|
|
21
|
-
"IQ3_XXS",
|
|
22
|
-
"IQ3_XS",
|
|
23
|
-
"IQ3_S",
|
|
24
|
-
"IQ3_M",
|
|
25
|
-
"IQ4_XS",
|
|
26
|
-
"IQ4_NL",
|
|
27
|
-
"Q2_K_S",
|
|
28
|
-
"Q2_K",
|
|
29
|
-
"Q3_K_S",
|
|
30
|
-
"Q3_K_M",
|
|
31
|
-
"Q3_K_L",
|
|
32
|
-
"Q4_K_S",
|
|
33
|
-
"Q4_K_M",
|
|
34
|
-
"Q5_K_S",
|
|
35
|
-
"Q5_K_M",
|
|
36
|
-
"Q6_K",
|
|
37
|
-
"Q8_0",
|
|
38
|
-
"Q4_0",
|
|
39
|
-
"Q4_1",
|
|
40
|
-
"Q5_0",
|
|
41
|
-
"Q5_1",
|
|
42
|
-
"NVFP4",
|
|
43
|
-
"MXFP4",
|
|
44
|
-
"BF16",
|
|
45
|
-
"F16",
|
|
46
|
-
"F32",
|
|
47
|
-
];
|
|
12
|
+
// The quant is the terminal GGUF filename suffix, optionally preceded by a
|
|
13
|
+
// source-defined qualifier such as Muse's `UD-`. Matching a known substring
|
|
14
|
+
// truncates newer labels (for example Q2_K_XL -> Q2_K) and misses them when
|
|
15
|
+
// their exact spelling is not in a hard-coded list.
|
|
16
|
+
const QUANT_SUFFIX = /(?:^|[-_])((?:UD-)?(?:IQ[1-4]|Q[2-8])(?:_[A-Z0-9]+)*|NVFP\d+|MXFP\d+|BF16|F16|F32)(?:-(?:MTP|IMATRIX|DISTILL))?(?:-\d{5}-OF-\d{5})?\.GGUF$/i;
|
|
48
17
|
const MULTIPART = /-(\d{5})-of-(\d{5})\.gguf$/i;
|
|
49
18
|
function walk(dir, out = []) {
|
|
50
19
|
let entries;
|
|
@@ -64,12 +33,9 @@ function walk(dir, out = []) {
|
|
|
64
33
|
return out;
|
|
65
34
|
}
|
|
66
35
|
export function detectQuant(filename) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return pattern;
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
36
|
+
const match = path.basename(filename).match(QUANT_SUFFIX);
|
|
37
|
+
// `UD-` is a source filename qualifier, not part of the user-facing quant.
|
|
38
|
+
return match?.[1]?.replace(/^UD-/i, "").toUpperCase() ?? null;
|
|
73
39
|
}
|
|
74
40
|
export function isProjectorFile(filename) {
|
|
75
41
|
return /^mmproj/i.test(path.basename(filename));
|
package/dist/runtime/args.js
CHANGED
|
@@ -52,6 +52,11 @@ export function buildArgs(profile, { port, host = "127.0.0.1" }) {
|
|
|
52
52
|
if (profile.vision && profile.mmprojPath) {
|
|
53
53
|
args.push("--mmproj", profile.mmprojPath);
|
|
54
54
|
}
|
|
55
|
+
// Component paths were resolved by the host from the catalog manifest. Only
|
|
56
|
+
// the known llama.cpp role is emitted; clients never supply process paths.
|
|
57
|
+
const drafter = profile.componentPaths?.speculative_drafter;
|
|
58
|
+
if (drafter)
|
|
59
|
+
args.push("--model-draft", drafter);
|
|
55
60
|
// The setting that was actually breaking long agentic runs.
|
|
56
61
|
if (profile.reasoningBudget !== null && profile.reasoningBudget !== undefined) {
|
|
57
62
|
args.push("--reasoning-budget", String(profile.reasoningBudget));
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Runtime } from "../types.js";
|
|
|
3
3
|
import { type InstallProgress, type RuntimeTarget } from "./managed.js";
|
|
4
4
|
export { BACKENDS_DIR, LMSTUDIO_ROOT, listRuntimes as listLmStudioRuntimes } from "./lmstudio.js";
|
|
5
5
|
export { buildArgs, buildEnv, formatCommand, type ServeTarget } from "./args.js";
|
|
6
|
-
export { installManagedRuntime, listManagedRuntimes, listRuntimeDevices, verifyRuntimeExecutable, defaultRuntimeSpec, extractArchive, resolveRuntimeVariant, serverExeName, supportedVariants, DEFAULT_LLAMA_BUILD, type RuntimeSpec, type RuntimeTarget, type RuntimeVariant, type InstallProgress, } from "./managed.js";
|
|
6
|
+
export { installManagedRuntime, removeManagedRuntime, listManagedRuntimes, listRuntimeDevices, verifyRuntimeExecutable, defaultRuntimeSpec, extractArchive, resolveRuntimeVariant, serverExeName, supportedVariants, DEFAULT_LLAMA_BUILD, listRuntimeReleases, latestRuntimeBuild, resolveLatestBuildOrPin, MissingAssetError, type ResolvedBuild, type RuntimeRelease, type RuntimeSpec, type RuntimeTarget, type RuntimeVariant, type InstallProgress, } from "./managed.js";
|
|
7
7
|
/** Every runtime available on this machine, managed first then LM Studio. */
|
|
8
8
|
export declare function listAllRuntimes(env?: NodeJS.ProcessEnv): Runtime[];
|
|
9
9
|
/**
|
package/dist/runtime/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { listRuntimes as listLmStudioRuntimes, resolveOverride } from "./lmstudi
|
|
|
16
16
|
import { defaultRuntimeSpec, installManagedRuntime, listManagedRuntimes, } from "./managed.js";
|
|
17
17
|
export { BACKENDS_DIR, LMSTUDIO_ROOT, listRuntimes as listLmStudioRuntimes } from "./lmstudio.js";
|
|
18
18
|
export { buildArgs, buildEnv, formatCommand } from "./args.js";
|
|
19
|
-
export { installManagedRuntime, listManagedRuntimes, listRuntimeDevices, verifyRuntimeExecutable, defaultRuntimeSpec, extractArchive, resolveRuntimeVariant, serverExeName, supportedVariants, DEFAULT_LLAMA_BUILD, } from "./managed.js";
|
|
19
|
+
export { installManagedRuntime, removeManagedRuntime, listManagedRuntimes, listRuntimeDevices, verifyRuntimeExecutable, defaultRuntimeSpec, extractArchive, resolveRuntimeVariant, serverExeName, supportedVariants, DEFAULT_LLAMA_BUILD, listRuntimeReleases, latestRuntimeBuild, resolveLatestBuildOrPin, MissingAssetError, } from "./managed.js";
|
|
20
20
|
/** Every runtime available on this machine, managed first then LM Studio. */
|
|
21
21
|
export function listAllRuntimes(env = process.env) {
|
|
22
22
|
const paths = resolveBrainPaths(env);
|
|
@@ -30,6 +30,33 @@ export interface InstallProgress {
|
|
|
30
30
|
* names (see the module header) - the naming scheme is not stable across tags.
|
|
31
31
|
*/
|
|
32
32
|
export declare const DEFAULT_LLAMA_BUILD = "b10265";
|
|
33
|
+
export interface RuntimeRelease {
|
|
34
|
+
build: string;
|
|
35
|
+
publishedAt: string | null;
|
|
36
|
+
}
|
|
37
|
+
/** Official llama.cpp builds, newest first. Only numbered build releases are actionable. */
|
|
38
|
+
export declare function listRuntimeReleases(limit?: number): Promise<RuntimeRelease[]>;
|
|
39
|
+
/** Resolve the latest official build at the last responsible moment. */
|
|
40
|
+
export declare function latestRuntimeBuild(): Promise<string>;
|
|
41
|
+
export interface ResolvedBuild {
|
|
42
|
+
build: string;
|
|
43
|
+
/** A single line explaining a fallback, or null when "latest" resolved. */
|
|
44
|
+
warning: string | null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The build to install for a "latest" request, with the pin as the safety net.
|
|
48
|
+
*
|
|
49
|
+
* Asking upstream is a best effort, never a precondition. `listRuntimeReleases`
|
|
50
|
+
* hits api.github.com unauthenticated, which is rate limited to 60 requests per
|
|
51
|
+
* hour per IP: behind NAT, on a corporate egress or on a CI runner that is a 403
|
|
52
|
+
* on an address that has spent its budget on something else entirely. An install
|
|
53
|
+
* the pinned build can serve must not fail because a version lookup did.
|
|
54
|
+
*
|
|
55
|
+
* The warning is deliberately one line. The daemon's BrainOpsManager keeps the
|
|
56
|
+
* *last* stderr line as the job's message, so a wrapped warning would surface in
|
|
57
|
+
* the GUI as a dangling fragment.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveLatestBuildOrPin(): Promise<ResolvedBuild>;
|
|
33
60
|
/** The binary name llama.cpp ships for a platform. */
|
|
34
61
|
export declare function serverExeName(platform?: NodeJS.Platform): string;
|
|
35
62
|
/**
|
|
@@ -49,6 +76,17 @@ export declare function supportedVariants(platform?: NodeJS.Platform, arch?: str
|
|
|
49
76
|
* best accelerator first and newest build first within an accelerator.
|
|
50
77
|
*/
|
|
51
78
|
export declare function listManagedRuntimes(runtimesDir: string, platform?: NodeJS.Platform): Runtime[];
|
|
79
|
+
/**
|
|
80
|
+
* An asset upstream does not serve at all, which is what a renamed asset or a
|
|
81
|
+
* tag without the expected build looks like from here. Distinguished from every
|
|
82
|
+
* other download failure because it is the one a caller can answer by retrying
|
|
83
|
+
* a build whose asset names are pinned and tested, rather than by retrying the
|
|
84
|
+
* same URL later.
|
|
85
|
+
*/
|
|
86
|
+
export declare class MissingAssetError extends Error {
|
|
87
|
+
readonly url: string;
|
|
88
|
+
constructor(url: string);
|
|
89
|
+
}
|
|
52
90
|
/**
|
|
53
91
|
* The actionable message for a dynamic-loader failure, or null when the output
|
|
54
92
|
* is not one. Pure, so the classification is testable without spawning.
|
|
@@ -87,4 +125,6 @@ export declare function listRuntimeDevices(runtime: Runtime, platform?: NodeJS.P
|
|
|
87
125
|
export declare function extractArchive(archivePath: string, destDir: string, platform?: NodeJS.Platform): Promise<void>;
|
|
88
126
|
/** Download + extract a runtime spec into runtimesDir and return the Runtime. */
|
|
89
127
|
export declare function installManagedRuntime(spec: RuntimeSpec, runtimesDir: string, onProgress?: (progress: InstallProgress) => void): Promise<Runtime>;
|
|
128
|
+
/** Remove one Otto-managed runtime. LM Studio files are outside this root. */
|
|
129
|
+
export declare function removeManagedRuntime(runtimesDir: string, name: string): void;
|
|
90
130
|
//# sourceMappingURL=managed.d.ts.map
|