offgrid-ai 0.8.12 → 0.8.13
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/package.json +1 -1
- package/src/commands/models.mjs +5 -13
- package/src/model-presenters.mjs +3 -2
- package/src/process.mjs +73 -8
- package/src/ui.mjs +1 -1
package/package.json
CHANGED
package/src/commands/models.mjs
CHANGED
|
@@ -46,7 +46,7 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
46
46
|
runningProfilesNow.push(profile);
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
49
|
-
if (await isProfileServerUp(profile)) serverUpIds.add(profile.id);
|
|
49
|
+
if (backendFor(profile.backend).type === "managed-server" && await isProfileServerUp(profile)) serverUpIds.add(profile.id);
|
|
50
50
|
}
|
|
51
51
|
printWorkspaceHeader(normalized, runningProfilesNow, serverUpIds);
|
|
52
52
|
await printBenchmarkLine();
|
|
@@ -63,22 +63,14 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
63
63
|
return "setup";
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
const groupOrder = [
|
|
67
|
-
|
|
68
|
-
{ key: "serverup", label: pc.yellow(" Server up · model not loaded") },
|
|
69
|
-
{ key: "ready", label: pc.blue(" Ready to chat") },
|
|
70
|
-
{ key: "setup", label: pc.yellow(" Need setup") },
|
|
71
|
-
{ key: "missing", label: pc.red(" File missing") },
|
|
72
|
-
];
|
|
73
|
-
const grouped = new Map(groupOrder.map((g) => [g.key, []]));
|
|
66
|
+
const groupOrder = ["running", "serverup", "ready", "setup", "missing"];
|
|
67
|
+
const grouped = new Map(groupOrder.map((key) => [key, []]));
|
|
74
68
|
for (const item of allItems) grouped.get(statusFor(item)).push(item);
|
|
75
69
|
|
|
76
|
-
const sectionSentinel = "__section__";
|
|
77
70
|
const choices = [];
|
|
78
71
|
for (const group of groupOrder) {
|
|
79
|
-
const bucket = grouped.get(group
|
|
72
|
+
const bucket = grouped.get(group);
|
|
80
73
|
if (!bucket || bucket.length === 0) continue;
|
|
81
|
-
choices.push({ value: `${sectionSentinel}:${group.key}`, label: `── ${group.label} (${bucket.length}) ──`, disabled: true });
|
|
82
74
|
for (const item of bucket) {
|
|
83
75
|
const opt = modelSelectOption(item, { runningProfilesNow, serverUpIds, nameWidth });
|
|
84
76
|
choices.push({ value: opt.value, label: opt.label });
|
|
@@ -88,7 +80,7 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
88
80
|
const prompt = createPrompt();
|
|
89
81
|
try {
|
|
90
82
|
const selected = await prompt.choice("Select a model", choices);
|
|
91
|
-
if (!selected
|
|
83
|
+
if (!selected) return;
|
|
92
84
|
const item = allItems.find((candidate) => itemKey(candidate) === selected);
|
|
93
85
|
if (!item) return;
|
|
94
86
|
|
package/src/model-presenters.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { BACKENDS, backendFor } from "./backends.mjs";
|
|
3
3
|
import { readCommandArgv } from "./profiles.mjs";
|
|
4
|
-
import { isProfileRunning } from "./process.mjs";
|
|
4
|
+
import { isProfileRunning, isProfileServerUp } from "./process.mjs";
|
|
5
5
|
import { buildPrettyCommand } from "./command.mjs";
|
|
6
6
|
import { pc, formatBytes, renderRows, renderSection } from "./ui.mjs";
|
|
7
7
|
import { capabilitySummary, ggufDetailParts, isProfileFileMissing, profileDetailParts } from "./model-summary.mjs";
|
|
@@ -160,10 +160,11 @@ export async function printProfileDetails(profile) {
|
|
|
160
160
|
const backend = backendFor(profile.backend);
|
|
161
161
|
const isManaged = backend.type === "managed-server";
|
|
162
162
|
const running = await isProfileRunning(profile);
|
|
163
|
+
const serverUp = !running && isManaged && await isProfileServerUp(profile);
|
|
163
164
|
const fileMissing = !isManaged && isProfileFileMissing(profile);
|
|
164
165
|
console.log("\n" + renderSection("Model overview", renderRows([
|
|
165
166
|
["Name", pc.bold(profile.label)],
|
|
166
|
-
["Status", fileMissing ? pc.red("File missing") : running ? pc.green("Running now") : pc.blue("Ready")],
|
|
167
|
+
["Status", fileMissing ? pc.red("File missing") : running ? pc.green("Running now") : serverUp ? pc.yellow("Server up, model not loaded") : pc.blue("Ready")],
|
|
167
168
|
["Details", profileDetailParts(profile, { fileMissing }).join(pc.dim(" · "))],
|
|
168
169
|
["Server", fileMissing ? pc.red(profile.baseUrl) : profile.baseUrl],
|
|
169
170
|
])));
|
package/src/process.mjs
CHANGED
|
@@ -125,6 +125,9 @@ export async function isProfileServerUp(profile) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
export async function modelLoadedOnServer(profile) {
|
|
128
|
+
const backend = backendFor(profile.backend);
|
|
129
|
+
if (backend.id === "ollama") return modelIdsMatch(await ollamaLoadedModelIds(profile), expectedModelIds(profile));
|
|
130
|
+
if (backend.id === "omlx") return modelIdsMatch(await omlxLoadedModelIds(profile), expectedModelIds(profile));
|
|
128
131
|
const { matches } = await serverMatchesProfile(profile);
|
|
129
132
|
return matches;
|
|
130
133
|
}
|
|
@@ -133,7 +136,8 @@ export async function profileRuntimeStatus(profile) {
|
|
|
133
136
|
const backend = backendFor(profile.backend);
|
|
134
137
|
if (backend.type === "managed-server") {
|
|
135
138
|
const ready = await serverReady(profile.baseUrl);
|
|
136
|
-
|
|
139
|
+
const modelLoaded = ready ? await modelLoadedOnServer(profile) : false;
|
|
140
|
+
return { state: null, pid: null, running: ready && modelLoaded, ready, serverUp: ready, modelLoaded, rssBytes: null, startedAt: null };
|
|
137
141
|
}
|
|
138
142
|
const state = await readState(profile.id);
|
|
139
143
|
const running = Boolean(state?.pid && pidAlive(state.pid));
|
|
@@ -191,18 +195,79 @@ export async function waitForReady(profile, pid, rawLogPath) {
|
|
|
191
195
|
// ── Internals ──────────────────────────────────────────────────────────────
|
|
192
196
|
|
|
193
197
|
async function serverModelIds(baseUrl) {
|
|
198
|
+
const body = await fetchJson(`${baseUrl.replace(/\/$/, "")}/models`);
|
|
199
|
+
return (Array.isArray(body?.data) ? body.data : [])
|
|
200
|
+
.map((model) => String(model?.id ?? "").trim())
|
|
201
|
+
.filter(Boolean);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function ollamaLoadedModelIds(profile) {
|
|
205
|
+
const body = await fetchJson(`${apiRootUrl(profile.baseUrl)}/api/ps`);
|
|
206
|
+
return (Array.isArray(body?.models) ? body.models : [])
|
|
207
|
+
.flatMap((model) => [model?.name, model?.model])
|
|
208
|
+
.map((id) => String(id ?? "").trim())
|
|
209
|
+
.filter(Boolean);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function omlxLoadedModelIds(profile) {
|
|
213
|
+
const status = await fetchJson(`${profile.baseUrl.replace(/\/$/, "")}/models/status`);
|
|
214
|
+
const fromStatus = (Array.isArray(status?.models) ? status.models : [])
|
|
215
|
+
.filter((model) => model?.loaded === true)
|
|
216
|
+
.flatMap((model) => [model?.id, model?.name, model?.model, model?.alias])
|
|
217
|
+
.map((id) => String(id ?? "").trim())
|
|
218
|
+
.filter(Boolean);
|
|
219
|
+
if (Number(status?.loaded_count) === 0) return fromStatus;
|
|
220
|
+
|
|
221
|
+
const summary = await fetchJson(`${apiRootUrl(profile.baseUrl)}/api/status`);
|
|
222
|
+
const fromSummary = (Array.isArray(summary?.loaded_models) ? summary.loaded_models : [])
|
|
223
|
+
.map((id) => String(id ?? "").trim())
|
|
224
|
+
.filter(Boolean);
|
|
225
|
+
return [...fromStatus, ...fromSummary];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function fetchJson(url) {
|
|
194
229
|
try {
|
|
195
|
-
const response = await fetch(
|
|
196
|
-
if (!response.ok) return
|
|
197
|
-
|
|
198
|
-
return (Array.isArray(body?.data) ? body.data : [])
|
|
199
|
-
.map((model) => String(model?.id ?? "").trim())
|
|
200
|
-
.filter(Boolean);
|
|
230
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1000) });
|
|
231
|
+
if (!response.ok) return null;
|
|
232
|
+
return await response.json();
|
|
201
233
|
} catch {
|
|
202
|
-
return
|
|
234
|
+
return null;
|
|
203
235
|
}
|
|
204
236
|
}
|
|
205
237
|
|
|
238
|
+
function apiRootUrl(baseUrl) {
|
|
239
|
+
try {
|
|
240
|
+
const url = new URL(baseUrl);
|
|
241
|
+
url.pathname = url.pathname.replace(/\/v1\/?$/u, "") || "/";
|
|
242
|
+
url.search = "";
|
|
243
|
+
url.hash = "";
|
|
244
|
+
return url.toString().replace(/\/$/u, "");
|
|
245
|
+
} catch {
|
|
246
|
+
return String(baseUrl).replace(/\/v1\/?$/u, "").replace(/\/$/u, "");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function modelIdsMatch(actualIds, expectedIds) {
|
|
251
|
+
const actual = normalizedModelIds(actualIds);
|
|
252
|
+
const expected = normalizedModelIds(expectedIds);
|
|
253
|
+
return [...expected].some((id) => actual.has(id));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizedModelIds(ids) {
|
|
257
|
+
const normalized = new Set();
|
|
258
|
+
for (const id of ids) {
|
|
259
|
+
const value = normalizeModelId(id);
|
|
260
|
+
if (!value) continue;
|
|
261
|
+
normalized.add(value);
|
|
262
|
+
if (value.endsWith(":latest")) normalized.add(value.slice(0, -":latest".length));
|
|
263
|
+
}
|
|
264
|
+
return normalized;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function normalizeModelId(id) {
|
|
268
|
+
return String(id ?? "").trim().toLowerCase();
|
|
269
|
+
}
|
|
270
|
+
|
|
206
271
|
function expectedModelIds(profile) {
|
|
207
272
|
const fileName = profile.modelPath ? basename(profile.modelPath) : null;
|
|
208
273
|
return [
|
package/src/ui.mjs
CHANGED
|
@@ -36,7 +36,7 @@ export function createPrompt() {
|
|
|
36
36
|
async choice(label, choices, defaultValue) {
|
|
37
37
|
return handleCancel(await select({
|
|
38
38
|
message: label, initialValue: defaultValue,
|
|
39
|
-
options: choices.map((c) => ({ value: c.value, label: c.label ?? c.value, hint: c.hint })),
|
|
39
|
+
options: choices.map((c) => ({ value: c.value, label: c.label ?? c.value, hint: c.hint, disabled: c.disabled })),
|
|
40
40
|
}));
|
|
41
41
|
},
|
|
42
42
|
close() {},
|