offgrid-ai 0.8.15 → 0.9.0
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 +3 -2
- package/src/backends.mjs +34 -38
- package/src/benchmark/finalize.mjs +198 -0
- package/src/benchmark/flow.mjs +237 -0
- package/src/benchmark/metrics.mjs +152 -0
- package/src/benchmark/pi-runner.mjs +252 -0
- package/src/benchmark/prepare.mjs +120 -0
- package/src/benchmark/repo.mjs +77 -0
- package/src/benchmark/shared.mjs +54 -0
- package/src/benchmark/stream-renderer.mjs +274 -0
- package/src/benchmark.mjs +10 -1330
- package/src/cli.mjs +2 -2
- package/src/commands/main.mjs +2 -2
- package/src/commands/onboard.mjs +6 -2
- package/src/config.mjs +8 -2
- package/src/harness-pi.mjs +1 -1
- package/src/managed.mjs +3 -3
- package/src/model-catalog.mjs +2 -1
- package/src/process.mjs +29 -21
- package/src/runtime.mjs +11 -0
package/src/cli.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { uninstallCommand } from "./commands/uninstall.mjs";
|
|
|
10
10
|
|
|
11
11
|
async function offerUpdate(argv) {
|
|
12
12
|
const invocation = detectInvocation();
|
|
13
|
-
const update = await checkForUpdate(
|
|
13
|
+
const update = await checkForUpdate();
|
|
14
14
|
if (!update) return false;
|
|
15
15
|
|
|
16
16
|
const plan = updateCommand(invocation, argv);
|
|
@@ -56,7 +56,7 @@ async function printVersion() {
|
|
|
56
56
|
const version = currentPackageVersion();
|
|
57
57
|
console.log(`offgrid-ai v${version}`);
|
|
58
58
|
const invocation = detectInvocation();
|
|
59
|
-
const update = await checkForUpdate(
|
|
59
|
+
const update = await checkForUpdate();
|
|
60
60
|
if (update) {
|
|
61
61
|
const plan = updateCommand(invocation, ["version"]);
|
|
62
62
|
console.log(pc.yellow(`Update available: v${update.latest}. Run: ${plan.display}`));
|
package/src/commands/main.mjs
CHANGED
|
@@ -27,8 +27,8 @@ export async function mainFlow() {
|
|
|
27
27
|
const { models: ggufModels, drafters } = await scanGgufModels();
|
|
28
28
|
const managedModels = await scanManagedModels();
|
|
29
29
|
const profiles = await loadProfiles();
|
|
30
|
-
const hasAnyBackend = llamaBinary || managedModels.some((item) => item.models.length > 0);
|
|
31
|
-
const hasAnyModels = ggufModels.length > 0 || managedModels.some((item) => item.models.length > 0);
|
|
30
|
+
const hasAnyBackend = llamaBinary || managedModels.some((item) => item.status === "ok" && item.models.length > 0);
|
|
31
|
+
const hasAnyModels = ggufModels.length > 0 || managedModels.some((item) => item.status === "ok" && item.models.length > 0);
|
|
32
32
|
|
|
33
33
|
const piInstalled = await hasPi();
|
|
34
34
|
const needsLlama = ggufModels.length > 0 || profiles.some((profile) => backendFor(profile.backend).type === "local-server");
|
package/src/commands/onboard.mjs
CHANGED
|
@@ -90,8 +90,12 @@ function printFoundModels(ggufModels, managedModels, llamaBinary) {
|
|
|
90
90
|
console.log(pc.green(`✓ Found ${ggufModels.length} GGUF model${ggufModels.length === 1 ? "" : "s"}`));
|
|
91
91
|
if (!llamaBinary) console.log(pc.yellow("Install the managed llama.cpp runtime to run these GGUF models."));
|
|
92
92
|
}
|
|
93
|
-
for (const { backendId, models } of managedModels) {
|
|
94
|
-
if (
|
|
93
|
+
for (const { backendId, models, status, reason } of managedModels) {
|
|
94
|
+
if (status === "unavailable") {
|
|
95
|
+
console.log(pc.yellow(`${BACKENDS[backendId].label}: unavailable${reason ? ` — ${reason}` : ""}`));
|
|
96
|
+
} else if (models.length > 0) {
|
|
97
|
+
console.log(pc.green(`✓ ${BACKENDS[backendId].label}: ${models.length} model${models.length === 1 ? "" : "s"}`));
|
|
98
|
+
}
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
|
package/src/config.mjs
CHANGED
|
@@ -46,8 +46,13 @@ export async function loadConfig() {
|
|
|
46
46
|
try {
|
|
47
47
|
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
48
48
|
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
49
|
-
} catch {
|
|
50
|
-
return { ...DEFAULT_CONFIG };
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error?.code === "ENOENT") return { ...DEFAULT_CONFIG };
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Failed to read config at ${CONFIG_PATH}: ${error.message}. ` +
|
|
53
|
+
`Fix or remove the file, then try again.`,
|
|
54
|
+
{ cause: error }
|
|
55
|
+
);
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -99,6 +104,7 @@ export async function findLlamaServer() {
|
|
|
99
104
|
if (existsSync(candidate)) return candidate;
|
|
100
105
|
} catch { /* Homebrew not installed or llama.cpp not brewed */ }
|
|
101
106
|
|
|
107
|
+
// No llama-server found — caller must present actionable error or onboarding.
|
|
102
108
|
return null;
|
|
103
109
|
}
|
|
104
110
|
|
package/src/harness-pi.mjs
CHANGED
|
@@ -70,7 +70,7 @@ export async function hasPi() {
|
|
|
70
70
|
// ── Internals ──────────────────────────────────────────────────────────────
|
|
71
71
|
|
|
72
72
|
async function activeProviderProfiles(currentProfile) {
|
|
73
|
-
const allProfiles = await loadProfiles()
|
|
73
|
+
const allProfiles = await loadProfiles();
|
|
74
74
|
const byAlias = new Map();
|
|
75
75
|
for (const item of [...allProfiles, currentProfile]) {
|
|
76
76
|
if (item.providerId !== currentProfile.providerId) continue;
|
package/src/managed.mjs
CHANGED
|
@@ -10,9 +10,9 @@ export async function scanManagedModels() {
|
|
|
10
10
|
const backend = BACKENDS[backendId];
|
|
11
11
|
try {
|
|
12
12
|
const models = await backend.scanModels();
|
|
13
|
-
results.push({ backendId, models });
|
|
14
|
-
} catch {
|
|
15
|
-
|
|
13
|
+
results.push({ backendId, models, status: "ok" });
|
|
14
|
+
} catch (error) {
|
|
15
|
+
results.push({ backendId, models: [], status: "unavailable", reason: error?.message ?? String(error) });
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
return results;
|
package/src/model-catalog.mjs
CHANGED
|
@@ -18,7 +18,8 @@ export function normalizeCatalog(catalog) {
|
|
|
18
18
|
const profiledPaths = new Set(profiles.map((profile) => profile.modelPath).filter(Boolean));
|
|
19
19
|
const newModels = ggufModels.filter((model) => !profiledPaths.has(model.path));
|
|
20
20
|
const managedItems = [];
|
|
21
|
-
for (const { backendId, models } of managedModels) {
|
|
21
|
+
for (const { backendId, models, status } of managedModels) {
|
|
22
|
+
if (status === "unavailable") continue;
|
|
22
23
|
const profiledAliases = new Set(
|
|
23
24
|
profiles
|
|
24
25
|
.filter((profile) => profile.backend === backendId)
|
package/src/process.mjs
CHANGED
|
@@ -195,47 +195,55 @@ export async function waitForReady(profile, pid, rawLogPath) {
|
|
|
195
195
|
// ── Internals ──────────────────────────────────────────────────────────────
|
|
196
196
|
|
|
197
197
|
async function serverModelIds(baseUrl) {
|
|
198
|
-
const
|
|
199
|
-
|
|
198
|
+
const result = await fetchJson(`${baseUrl.replace(/\/+$/u, "")}/models`);
|
|
199
|
+
if (!result.ok) return [];
|
|
200
|
+
return (Array.isArray(result.data?.data) ? result.data.data : [])
|
|
200
201
|
.map((model) => String(model?.id ?? "").trim())
|
|
201
202
|
.filter(Boolean);
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
async function ollamaLoadedModelIds(profile) {
|
|
205
|
-
const
|
|
206
|
-
|
|
206
|
+
const result = await fetchJson(`${apiRootUrl(profile.baseUrl)}/api/ps`);
|
|
207
|
+
if (!result.ok) return [];
|
|
208
|
+
return (Array.isArray(result.data?.models) ? result.data.models : [])
|
|
207
209
|
.flatMap((model) => [model?.name, model?.model])
|
|
208
210
|
.map((id) => String(id ?? "").trim())
|
|
209
211
|
.filter(Boolean);
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
async function omlxLoadedModelIds(profile) {
|
|
213
|
-
const
|
|
214
|
-
const fromStatus =
|
|
215
|
-
.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
215
|
+
const statusResult = await fetchJson(`${profile.baseUrl.replace(/\/+$/u, "")}/models/status`);
|
|
216
|
+
const fromStatus = statusResult.ok
|
|
217
|
+
? (Array.isArray(statusResult.data?.models) ? statusResult.data.models : [])
|
|
218
|
+
.filter((model) => model?.loaded === true)
|
|
219
|
+
.flatMap((model) => [model?.id, model?.name, model?.model, model?.alias])
|
|
220
|
+
.map((id) => String(id ?? "").trim())
|
|
221
|
+
.filter(Boolean)
|
|
222
|
+
: [];
|
|
223
|
+
if (!statusResult.ok || Number(statusResult.data?.loaded_count) === 0) return fromStatus;
|
|
224
|
+
|
|
225
|
+
const summaryResult = await fetchJson(`${apiRootUrl(profile.baseUrl)}/api/status`);
|
|
226
|
+
const fromSummary = summaryResult.ok
|
|
227
|
+
? (Array.isArray(summaryResult.data?.loaded_models) ? summaryResult.data.loaded_models : [])
|
|
228
|
+
.map((id) => String(id ?? "").trim())
|
|
229
|
+
.filter(Boolean)
|
|
230
|
+
: [];
|
|
225
231
|
return [...fromStatus, ...fromSummary];
|
|
226
232
|
}
|
|
227
233
|
|
|
228
234
|
async function fetchJson(url) {
|
|
229
235
|
try {
|
|
230
236
|
const response = await fetch(url, { signal: AbortSignal.timeout(1000) });
|
|
231
|
-
if (!response.ok) return null;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
237
|
+
if (!response.ok) return { ok: false, reason: "http", status: response.status, data: null };
|
|
238
|
+
const data = await response.json();
|
|
239
|
+
return { ok: true, data };
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error?.name === "AbortError" || error?.name === "TimeoutError") return { ok: false, reason: "timeout", data: null };
|
|
242
|
+
return { ok: false, reason: "network", data: null };
|
|
235
243
|
}
|
|
236
244
|
}
|
|
237
245
|
|
|
238
|
-
function apiRootUrl(baseUrl) {
|
|
246
|
+
export function apiRootUrl(baseUrl) {
|
|
239
247
|
try {
|
|
240
248
|
const url = new URL(baseUrl);
|
|
241
249
|
url.pathname = url.pathname.replace(/\/v1\/?$/u, "") || "/";
|
package/src/runtime.mjs
CHANGED
|
@@ -33,6 +33,12 @@ export async function offerManagedLlamaRuntimeUpdate(prompt, { fetchImpl = globa
|
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Check for the latest llama.cpp release on GitHub.
|
|
38
|
+
* Returns null if the check fails (network error, timeout, etc.).
|
|
39
|
+
* Callers must treat null as "check failed, skip prompt" — do not
|
|
40
|
+
* treat null as "no update available."
|
|
41
|
+
*/
|
|
36
42
|
export async function latestLlamaRelease(fetchImpl = globalThis.fetch) {
|
|
37
43
|
try {
|
|
38
44
|
const response = await fetchImpl(RELEASE_API, { signal: AbortSignal.timeout(5000) });
|
|
@@ -47,6 +53,11 @@ export async function latestLlamaRelease(fetchImpl = globalThis.fetch) {
|
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Read the installed runtime version from disk.
|
|
58
|
+
* Returns null if not installed or if the version file is missing/corrupt.
|
|
59
|
+
* Callers must treat null as "runtime not installed" — not as a hidden default.
|
|
60
|
+
*/
|
|
50
61
|
export async function installedRuntime() {
|
|
51
62
|
try {
|
|
52
63
|
return JSON.parse(await readFile(VERSION_PATH, "utf8"));
|