offgrid-ai 0.9.2 → 0.9.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/package.json
CHANGED
package/src/backends.mjs
CHANGED
|
@@ -143,7 +143,7 @@ function isLocalOllamaModel(model) {
|
|
|
143
143
|
function isChatOmlxModel(model) {
|
|
144
144
|
if (typeof model?.id !== "string" || !model.id.trim()) return false;
|
|
145
145
|
const type = String(model.type ?? model.model_type ?? "").toLowerCase();
|
|
146
|
-
if (["embedding", "embeddings", "reranker", "tool", "converter"].includes(type)) return false;
|
|
146
|
+
if (["embedding", "embeddings", "reranker", "tool", "converter", "markitdown"].includes(type)) return false;
|
|
147
147
|
if (Object.hasOwn(model, "max_model_len") && model.max_model_len === null) return false;
|
|
148
148
|
return true;
|
|
149
149
|
}
|
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import {
|
|
7
7
|
BENCH_COLORS, renderStreamEvent,
|
|
8
|
-
formatToolCall,
|
|
8
|
+
formatToolCall, printFinalLine,
|
|
9
9
|
} from "./stream-renderer.mjs";
|
|
10
10
|
import { piModelString } from "./shared.mjs";
|
|
11
11
|
|
|
@@ -212,6 +212,8 @@ export async function runBenchmarkInPi(profile, runDirectory, { signal } = {}) {
|
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
printFinalLine(BENCH_COLORS.info("Pi benchmark finished"));
|
|
216
|
+
|
|
215
217
|
if (runResult.exitCode !== 0) {
|
|
216
218
|
runResult.error = { message: `Pi exited with code ${runResult.exitCode}` };
|
|
217
219
|
resolve(runResult);
|
package/src/commands/run.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { ensureDirs } from "../config.mjs";
|
|
3
3
|
import { backendFor } from "../backends.mjs";
|
|
4
4
|
import { normalizeProfile, readProfile, saveProfile } from "../profiles.mjs";
|
|
5
|
-
import { startServer, stopProfile, waitForReady, serverReady, serverMatchesProfile } from "../process.mjs";
|
|
5
|
+
import { startServer, stopProfile, waitForReady, serverReady, serverMatchesProfile, modelAvailableOnServer } from "../process.mjs";
|
|
6
6
|
import { syncPiConfig, hasPiModel, launchPi, hasPi } from "../harness-pi.mjs";
|
|
7
7
|
import { tailFriendly } from "../logs.mjs";
|
|
8
8
|
import { estimateMemory } from "../estimate.mjs";
|
|
@@ -33,6 +33,11 @@ export async function runProfile(profile, options = {}) {
|
|
|
33
33
|
if (!(await serverReady(profile.baseUrl))) {
|
|
34
34
|
throw new Error(`${backend.label} is not running at ${profile.baseUrl}. Start it and try again.`);
|
|
35
35
|
}
|
|
36
|
+
const available = await modelAvailableOnServer(profile);
|
|
37
|
+
if (!available) {
|
|
38
|
+
const modelId = profile.omlxModel ?? profile.ollamaModel ?? profile.modelAlias ?? profile.label;
|
|
39
|
+
throw new Error(`${modelId} is not available on ${backend.label} at ${profile.baseUrl}.`);
|
|
40
|
+
}
|
|
36
41
|
console.log(pc.green(`[ready] ${backend.label} at ${profile.baseUrl}`));
|
|
37
42
|
} else {
|
|
38
43
|
const startup = await ensureLocalServer(profile, backend, options);
|
package/src/commands/status.mjs
CHANGED
|
@@ -13,17 +13,48 @@ export async function statusCommand() {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const running = statuses.filter((item) => item.status.running);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return;
|
|
16
|
+
const managedUpMissing = statuses.filter((item) => {
|
|
17
|
+
const backend = backendFor(item.profile.backend);
|
|
18
|
+
return backend.type === "managed-server" && item.status.serverUp && !item.status.modelAvailable;
|
|
19
|
+
});
|
|
20
|
+
const managedUpNotLoaded = statuses.filter((item) => {
|
|
21
|
+
const backend = backendFor(item.profile.backend);
|
|
22
|
+
return backend.type === "managed-server" && item.status.serverUp && item.status.modelAvailable && !item.status.modelLoaded;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const summaryRows = [
|
|
26
|
+
["Running now", running.length > 0 ? pc.green(`${running.length} model${running.length === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
27
|
+
["Ready setups", profiles.length > 0 ? String(profiles.length) : pc.dim("none")],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
if (managedUpMissing.length > 0) {
|
|
31
|
+
summaryRows.push(["Server up, model missing", pc.yellow(String(managedUpMissing.length))]);
|
|
32
|
+
}
|
|
33
|
+
if (managedUpNotLoaded.length > 0) {
|
|
34
|
+
summaryRows.push(["Server up, model not loaded", pc.yellow(String(managedUpNotLoaded.length))]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
summaryRows.push(["Next step", profiles.length > 0 ? "Run offgrid-ai to start chatting" : pc.yellow("Run offgrid-ai to set up a model")]);
|
|
38
|
+
|
|
39
|
+
console.log(renderCard("Status", renderRows(summaryRows), { formatBorder: running.length > 0 ? pc.green : pc.dim }));
|
|
40
|
+
|
|
41
|
+
if (managedUpMissing.length > 0 || managedUpNotLoaded.length > 0) {
|
|
42
|
+
const detailRows = [];
|
|
43
|
+
for (const { profile, status } of [...managedUpMissing, ...managedUpNotLoaded]) {
|
|
44
|
+
const backend = backendFor(profile.backend);
|
|
45
|
+
const modelId = profile.omlxModel ?? profile.ollamaModel ?? profile.modelAlias ?? profile.id;
|
|
46
|
+
const state = status.modelAvailable
|
|
47
|
+
? pc.yellow("server up · model not loaded")
|
|
48
|
+
: pc.red("server up · model missing");
|
|
49
|
+
detailRows.push([`${profile.label} (${modelId})`, state]);
|
|
50
|
+
detailRows.push(["Server", `${backend.label} at ${profile.baseUrl}`]);
|
|
51
|
+
}
|
|
52
|
+
console.log("\n" + renderCard("Managed servers", renderRows(detailRows), { formatBorder: pc.yellow }));
|
|
23
53
|
}
|
|
24
54
|
|
|
25
|
-
|
|
26
|
-
|
|
55
|
+
if (running.length === 0) return;
|
|
56
|
+
|
|
57
|
+
console.log("\n" + renderCard("Running", renderRows([
|
|
27
58
|
["Stop", "offgrid-ai stop"],
|
|
28
59
|
]), { formatBorder: pc.green }));
|
|
29
60
|
for (const { profile, status } of running) {
|
package/src/process.mjs
CHANGED
|
@@ -132,12 +132,27 @@ export async function modelLoadedOnServer(profile) {
|
|
|
132
132
|
return matches;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
export async function modelAvailableOnServer(profile) {
|
|
136
|
+
const backend = backendFor(profile.backend);
|
|
137
|
+
if (backend.id === "ollama") {
|
|
138
|
+
return modelIdsMatch(await ollamaAvailableModelIds(profile), expectedModelIds(profile));
|
|
139
|
+
}
|
|
140
|
+
if (backend.id === "omlx") {
|
|
141
|
+
// /v1/models lists discovered models; an ID must exist there to be usable.
|
|
142
|
+
return modelIdsMatch(await serverModelIds(profile.baseUrl), expectedModelIds(profile));
|
|
143
|
+
}
|
|
144
|
+
// Local servers are tied to a specific model file via their command argv.
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
135
148
|
export async function profileRuntimeStatus(profile) {
|
|
136
149
|
const backend = backendFor(profile.backend);
|
|
137
150
|
if (backend.type === "managed-server") {
|
|
138
151
|
const ready = await serverReady(profile.baseUrl);
|
|
139
|
-
const modelLoaded = ready
|
|
140
|
-
|
|
152
|
+
const [modelLoaded, modelAvailable] = ready
|
|
153
|
+
? await Promise.all([modelLoadedOnServer(profile), modelAvailableOnServer(profile)])
|
|
154
|
+
: [false, false];
|
|
155
|
+
return { state: null, pid: null, running: ready && modelLoaded, ready, serverUp: ready, modelLoaded, modelAvailable, rssBytes: null, startedAt: null };
|
|
141
156
|
}
|
|
142
157
|
const state = await readState(profile.id);
|
|
143
158
|
const running = Boolean(state?.pid && pidAlive(state.pid));
|
|
@@ -211,6 +226,15 @@ async function ollamaLoadedModelIds(profile) {
|
|
|
211
226
|
.filter(Boolean);
|
|
212
227
|
}
|
|
213
228
|
|
|
229
|
+
async function ollamaAvailableModelIds(profile) {
|
|
230
|
+
const result = await fetchJson(`${apiRootUrl(profile.baseUrl)}/api/tags`);
|
|
231
|
+
if (!result.ok) return [];
|
|
232
|
+
return (Array.isArray(result.data?.models) ? result.data.models : [])
|
|
233
|
+
.flatMap((model) => [model?.name, model?.model])
|
|
234
|
+
.map((id) => String(id ?? "").trim())
|
|
235
|
+
.filter(Boolean);
|
|
236
|
+
}
|
|
237
|
+
|
|
214
238
|
async function omlxLoadedModelIds(profile) {
|
|
215
239
|
const statusResult = await fetchJson(`${profile.baseUrl.replace(/\/+$/u, "")}/models/status`);
|
|
216
240
|
const fromStatus = statusResult.ok
|