offgrid-ai 0.14.0 → 0.14.1
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/benchmark/flow.mjs +28 -33
- package/src/benchmark/prepare.mjs +2 -2
- package/src/benchmark/sdk-runner.mjs +5 -22
- package/src/benchmark/shared.mjs +0 -8
- package/src/benchmark.mjs +1 -1
- package/src/commands/run.mjs +0 -12
- package/src/harness-pi.mjs +3 -3
- package/src/model-presenters.mjs +0 -15
- package/src/process.mjs +5 -1
- package/src/profile-setup.mjs +8 -73
- package/src/profiles.mjs +4 -21
- package/src/command.mjs +0 -21
package/package.json
CHANGED
package/src/benchmark/flow.mjs
CHANGED
|
@@ -128,6 +128,28 @@ export async function runPreparedBenchmark(profile, runDirectory, options = {})
|
|
|
128
128
|
return metadata;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// ── Shared benchmark selection ────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
async function selectBenchmark(prompt, repoPath) {
|
|
134
|
+
const kind = await prompt.choice("Benchmark category", [
|
|
135
|
+
{ value: "visual", label: "Visual Benchmark", hint: "HTML/CSS/JS animation benchmarks" },
|
|
136
|
+
{ value: "data-science", label: "Data Science", hint: "Analysis and charting benchmarks" },
|
|
137
|
+
], "visual");
|
|
138
|
+
|
|
139
|
+
const benchDir = join(repoPath, "benchmarks");
|
|
140
|
+
const benchmarks = (await loadBenchmarks(benchDir)).filter((b) => b.kind === kind);
|
|
141
|
+
if (benchmarks.length === 0) {
|
|
142
|
+
console.log(pc.yellow(`No ${kind} benchmarks found in ${benchDir}`));
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const benchmarkId = await prompt.choice("Prompt", benchmarks.map((b) => ({
|
|
146
|
+
value: b.id, label: b.title, hint: b.description || b.id,
|
|
147
|
+
})), benchmarks[0].id);
|
|
148
|
+
const benchmark = benchmarks.find((b) => b.id === benchmarkId);
|
|
149
|
+
if (!benchmark) return null;
|
|
150
|
+
return { kind, benchmark };
|
|
151
|
+
}
|
|
152
|
+
|
|
131
153
|
// ── Benchmark from a selected profile (from model picker) ────────────────
|
|
132
154
|
|
|
133
155
|
export async function benchmarkForProfile(profile) {
|
|
@@ -137,22 +159,9 @@ export async function benchmarkForProfile(profile) {
|
|
|
137
159
|
const repoPath = await linkBenchmarkRepo(prompt);
|
|
138
160
|
if (!repoPath) return;
|
|
139
161
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
], "visual");
|
|
144
|
-
|
|
145
|
-
const benchDir = join(repoPath, "benchmarks");
|
|
146
|
-
const benchmarks = (await loadBenchmarks(benchDir)).filter((b) => b.kind === kind);
|
|
147
|
-
if (benchmarks.length === 0) {
|
|
148
|
-
console.log(pc.yellow(`No ${kind} benchmarks found in ${benchDir}`));
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const benchmarkId = await prompt.choice("Prompt", benchmarks.map((b) => ({
|
|
152
|
-
value: b.id, label: b.title, hint: b.description || b.id,
|
|
153
|
-
})), benchmarks[0].id);
|
|
154
|
-
const selectedBenchmark = benchmarks.find((b) => b.id === benchmarkId);
|
|
155
|
-
if (!selectedBenchmark) return;
|
|
162
|
+
const selected = await selectBenchmark(prompt, repoPath);
|
|
163
|
+
if (!selected) return;
|
|
164
|
+
const { kind, benchmark: selectedBenchmark } = selected;
|
|
156
165
|
|
|
157
166
|
const modelId = profile.modelAlias;
|
|
158
167
|
const modelSource = benchmarkModelSource(profile);
|
|
@@ -177,28 +186,14 @@ export async function benchmarkForProfile(profile) {
|
|
|
177
186
|
|
|
178
187
|
export async function benchmarkFlow() {
|
|
179
188
|
await ensureDirs();
|
|
180
|
-
|
|
181
189
|
const prompt = createPrompt();
|
|
182
190
|
try {
|
|
183
191
|
const repoPath = await linkBenchmarkRepo(prompt);
|
|
184
192
|
if (!repoPath) return;
|
|
185
193
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
], "visual");
|
|
190
|
-
|
|
191
|
-
const benchDir = join(repoPath, "benchmarks");
|
|
192
|
-
const benchmarks = (await loadBenchmarks(benchDir)).filter((b) => b.kind === kind);
|
|
193
|
-
if (benchmarks.length === 0) {
|
|
194
|
-
console.log(pc.yellow(`No ${kind} benchmarks found in ${benchDir}`));
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
const benchmarkId = await prompt.choice("Prompt", benchmarks.map((b) => ({
|
|
198
|
-
value: b.id, label: b.title, hint: b.description || b.id,
|
|
199
|
-
})), benchmarks[0].id);
|
|
200
|
-
const selectedBenchmark = benchmarks.find((b) => b.id === benchmarkId);
|
|
201
|
-
if (!selectedBenchmark) return;
|
|
194
|
+
const selected = await selectBenchmark(prompt, repoPath);
|
|
195
|
+
if (!selected) return;
|
|
196
|
+
const { kind, benchmark: selectedBenchmark } = selected;
|
|
202
197
|
|
|
203
198
|
const profiles = await loadProfiles();
|
|
204
199
|
const source = await prompt.choice("Model source", [
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { pc, renderRows, renderSection } from "../ui.mjs";
|
|
6
|
-
import { slugModelId, createRunId
|
|
6
|
+
import { slugModelId, createRunId } from "./shared.mjs";
|
|
7
7
|
import { parseModelName } from "../model-name.mjs";
|
|
8
8
|
|
|
9
9
|
function harnessDisplayName(id) {
|
|
@@ -30,7 +30,7 @@ function printBenchmarkNextSteps({ repoPath, runDirectory, profile, modelId, run
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSource, backendLabel, profile, showNextSteps = true }) {
|
|
33
|
-
const toolPrompt =
|
|
33
|
+
const toolPrompt = benchmark.prompt;
|
|
34
34
|
const now = new Date();
|
|
35
35
|
const runId = createRunId(now);
|
|
36
36
|
const modelSlug = slugModelId(modelId);
|
|
@@ -5,8 +5,8 @@ import { join, relative, basename } from "node:path";
|
|
|
5
5
|
import { Agent } from "@earendil-works/pi-agent-core";
|
|
6
6
|
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
7
7
|
import { createCodingTools } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { pc } from "../ui.mjs";
|
|
9
|
-
import { piApiModelId } from "../harness-pi.mjs";
|
|
8
|
+
import { pc, formatBytes } from "../ui.mjs";
|
|
9
|
+
import { piApiModelId, modelReasoning, modelCompat } from "../harness-pi.mjs";
|
|
10
10
|
|
|
11
11
|
const C = {
|
|
12
12
|
thinking: pc.magenta,
|
|
@@ -257,16 +257,8 @@ export async function runBenchmarkInPi(profile, runDirectory, { signal } = {}) {
|
|
|
257
257
|
// ── Model construction ──────────────────────────────────────────────────────
|
|
258
258
|
|
|
259
259
|
function buildModel(profile) {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
const reasoning = profile.reasoning !== undefined
|
|
263
|
-
? Boolean(profile.reasoning)
|
|
264
|
-
: /qwen|gemma-4|gemma 4/i.test(text);
|
|
265
|
-
const hasCompat = profile.compat
|
|
266
|
-
? profile.compat
|
|
267
|
-
: /qwen|gemma-4|gemma 4/i.test(text)
|
|
268
|
-
? { thinkingFormat: "qwen-chat-template" }
|
|
269
|
-
: null;
|
|
260
|
+
const reasoning = modelReasoning(profile) ?? false;
|
|
261
|
+
const compat = modelCompat(profile);
|
|
270
262
|
|
|
271
263
|
return {
|
|
272
264
|
id: piApiModelId(profile),
|
|
@@ -283,7 +275,7 @@ function buildModel(profile) {
|
|
|
283
275
|
supportsDeveloperRole: false,
|
|
284
276
|
supportsReasoningEffort: false,
|
|
285
277
|
maxTokensField: "max_tokens",
|
|
286
|
-
...(
|
|
278
|
+
...(compat ?? {}),
|
|
287
279
|
},
|
|
288
280
|
};
|
|
289
281
|
}
|
|
@@ -364,15 +356,6 @@ function truncateOneLine(value, max = 80) {
|
|
|
364
356
|
return text.length > max ? `${text.slice(0, Math.max(1, max - 1))}…` : text;
|
|
365
357
|
}
|
|
366
358
|
|
|
367
|
-
function formatBytes(bytes) {
|
|
368
|
-
if (!Number.isFinite(bytes)) return "unknown";
|
|
369
|
-
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
370
|
-
let size = bytes;
|
|
371
|
-
let unit = 0;
|
|
372
|
-
while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit += 1; }
|
|
373
|
-
return `${size.toFixed(unit === 0 ? 0 : 2)} ${units[unit]}`;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
359
|
function formatTokens(n) {
|
|
377
360
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
378
361
|
if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
|
package/src/benchmark/shared.mjs
CHANGED
|
@@ -18,10 +18,6 @@ export function createRunId(date = new Date()) {
|
|
|
18
18
|
return date.toISOString().replace(/:/gu, "-").replace(/\./gu, "-");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function buildToolPrompt(benchmark) {
|
|
22
|
-
return benchmark.prompt;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
export async function loadBenchmarks(benchDir) {
|
|
26
22
|
const entries = await readdir(benchDir);
|
|
27
23
|
const markdownFiles = entries.filter((f) => f.endsWith(".md")).sort();
|
|
@@ -47,8 +43,4 @@ export async function loadBenchmarks(benchDir) {
|
|
|
47
43
|
benchmarks.push({ id, title, description, prompt: content, kind });
|
|
48
44
|
}
|
|
49
45
|
return benchmarks;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function piModelString(profile) {
|
|
53
|
-
return profile.harnesses?.pi?.model ?? `${profile.providerId}/${profile.modelAlias}`;
|
|
54
46
|
}
|
package/src/benchmark.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── Benchmark module (thin facade) ──────────────────────────────────────────
|
|
2
2
|
// Submodules handle the actual logic. This file re-exports for backward compatibility.
|
|
3
3
|
|
|
4
|
-
export { slugModelId, createRunId,
|
|
4
|
+
export { slugModelId, createRunId, loadBenchmarks } from "./benchmark/shared.mjs";
|
|
5
5
|
export { findBenchmarkRepo, linkBenchmarkRepo } from "./benchmark/repo.mjs";
|
|
6
6
|
export { prepareBenchmarkRun } from "./benchmark/prepare.mjs";
|
|
7
7
|
export { runBenchmarkInPi } from "./benchmark/sdk-runner.mjs";
|
package/src/commands/run.mjs
CHANGED
|
@@ -149,18 +149,6 @@ function textOnlyProfile(profile) {
|
|
|
149
149
|
mmprojPath: null,
|
|
150
150
|
disabledMmprojPath: profile.disabledMmprojPath ?? profile.mmprojPath,
|
|
151
151
|
capabilities: { ...(profile.capabilities ?? {}), vision: false, visionDisabledReason: "unsupported-mmproj" },
|
|
152
|
-
commandArgv: removeCommandOption(profile.commandArgv ?? [], "--mmproj"),
|
|
153
152
|
});
|
|
154
153
|
}
|
|
155
154
|
|
|
156
|
-
function removeCommandOption(argv, flag) {
|
|
157
|
-
const next = [];
|
|
158
|
-
for (let i = 0; i < argv.length; i++) {
|
|
159
|
-
if (argv[i] === flag) {
|
|
160
|
-
if (argv[i + 1] && !argv[i + 1].startsWith("--")) i += 1;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
next.push(argv[i]);
|
|
164
|
-
}
|
|
165
|
-
return next;
|
|
166
|
-
}
|
package/src/harness-pi.mjs
CHANGED
|
@@ -118,7 +118,7 @@ function modelInput(profile) {
|
|
|
118
118
|
return ["text"];
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
function modelCompat(profile) {
|
|
121
|
+
export function modelCompat(profile) {
|
|
122
122
|
if (profile.compat) return profile.compat;
|
|
123
123
|
const family = modelFamily(profile);
|
|
124
124
|
if (family.includes("qwen") || family.includes("gemma-4") || family.includes("gemma 4")) {
|
|
@@ -127,14 +127,14 @@ function modelCompat(profile) {
|
|
|
127
127
|
return null;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
function modelReasoning(profile) {
|
|
130
|
+
export function modelReasoning(profile) {
|
|
131
131
|
if (profile.reasoning !== undefined) return Boolean(profile.reasoning);
|
|
132
132
|
const family = modelFamily(profile);
|
|
133
133
|
if (family.includes("qwen") || family.includes("gemma-4") || family.includes("gemma 4")) return true;
|
|
134
134
|
return undefined;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
function modelFamily(profile) {
|
|
137
|
+
export function modelFamily(profile) {
|
|
138
138
|
return [profile.id, profile.label, profile.modelAlias, profile.modelPath, profile.omlxModel].filter(Boolean).join(" ").toLowerCase();
|
|
139
139
|
}
|
|
140
140
|
|
package/src/model-presenters.mjs
CHANGED
|
@@ -159,21 +159,6 @@ export function modelSelectOption(item, { runningProfilesNow, modelMissingIds, n
|
|
|
159
159
|
...(hint ? { hint: pc.red(hint) } : {}),
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
|
-
if (item.type === "new") {
|
|
163
|
-
return {
|
|
164
|
-
value: itemKey(item),
|
|
165
|
-
label: optionLabel({
|
|
166
|
-
status: optionStatusTag("setup"),
|
|
167
|
-
backend: optionBackendTag(backendId),
|
|
168
|
-
source: optionSourceTag(sourceId),
|
|
169
|
-
name: item.label,
|
|
170
|
-
nameWidth,
|
|
171
|
-
quant: optionQuantLabel(item),
|
|
172
|
-
ctx: optionCtxLabel(item),
|
|
173
|
-
size: optionSizeLabel(item),
|
|
174
|
-
}),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
162
|
return {
|
|
178
163
|
value: itemKey(item),
|
|
179
164
|
label: optionLabel({
|
package/src/process.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import { promisify } from "node:util";
|
|
|
3
3
|
import { closeSync, openSync } from "node:fs";
|
|
4
4
|
import { readFile, writeFile, chmod } from "node:fs/promises";
|
|
5
5
|
import { basename, join } from "node:path";
|
|
6
|
-
import { quoteShell } from "./command.mjs";
|
|
7
6
|
import { LOG_DIR } from "./config.mjs";
|
|
8
7
|
import { writeState, readState, profileDir } from "./profiles.mjs";
|
|
9
8
|
import { backendFor, backendBinaryFor } from "./backends.mjs";
|
|
@@ -501,4 +500,9 @@ function timestampForFile() {
|
|
|
501
500
|
|
|
502
501
|
function sleep(ms) {
|
|
503
502
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function quoteShell(value) {
|
|
506
|
+
const text = String(value);
|
|
507
|
+
return /^[A-Za-z0-9_/@%+=:,.-]+$/u.test(text) ? text : `'${text.replace(/'/gu, ` '"'"'`)}'`;
|
|
504
508
|
}
|
package/src/profile-setup.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { detectCapabilities } from "./autodetect.mjs";
|
|
|
9
9
|
import { matchDrafter } from "./scan.mjs";
|
|
10
10
|
import { scanGgufModels } from "./scan.mjs";
|
|
11
11
|
import { estimateMemoryMb } from "./mlx-flags.mjs";
|
|
12
|
+
import { capabilitySummary } from "./model-summary.mjs";
|
|
12
13
|
|
|
13
14
|
const execFileAsync = promisify(execFile);
|
|
14
15
|
|
|
@@ -70,7 +71,7 @@ export async function configureLocalProfile(prompt, profile) {
|
|
|
70
71
|
console.log("");
|
|
71
72
|
console.log(renderSection("Model setup", renderRows([
|
|
72
73
|
["Model", pc.bold(profile.label)],
|
|
73
|
-
["Detected",
|
|
74
|
+
["Detected", capabilitySummary(caps)],
|
|
74
75
|
["Context", `${profile.flags.ctxSize.toLocaleString()} tokens`],
|
|
75
76
|
["KV cache", `${profile.flags.cacheTypeK}/${profile.flags.cacheTypeV}`],
|
|
76
77
|
["Sampling", samplingSummary(profile.flags)],
|
|
@@ -151,16 +152,12 @@ export function applyRuntimeFlagOverrides(profile, overrides) {
|
|
|
151
152
|
|
|
152
153
|
export function applyMtpDefaults(profile) {
|
|
153
154
|
const flags = { ...profile.flags, port: LLAMA_CPP_MTP_PORT };
|
|
154
|
-
const edits = {
|
|
155
|
-
values: { "--spec-type": "draft-mtp", "--spec-draft-n-max": 4 },
|
|
156
|
-
};
|
|
157
|
-
if (profile.drafterPath) edits.values["--spec-draft-model"] = profile.drafterPath;
|
|
158
155
|
return applyProfileFlags({
|
|
159
156
|
...profile,
|
|
160
157
|
backend: "llama-cpp-mtp",
|
|
161
158
|
providerId: "llama-cpp-mtp",
|
|
162
159
|
capabilities: { ...(profile.capabilities ?? {}), mtp: true },
|
|
163
|
-
}, flags
|
|
160
|
+
}, flags);
|
|
164
161
|
}
|
|
165
162
|
|
|
166
163
|
export function removeMtpDefaults(profile) {
|
|
@@ -171,9 +168,7 @@ export function removeMtpDefaults(profile) {
|
|
|
171
168
|
providerId: "llama-cpp",
|
|
172
169
|
drafterPath: null,
|
|
173
170
|
capabilities: { ...(profile.capabilities ?? {}), mtp: false },
|
|
174
|
-
}, flags
|
|
175
|
-
remove: ["--spec-type", "--spec-draft-n-max", "--spec-draft-model"],
|
|
176
|
-
});
|
|
171
|
+
}, flags);
|
|
177
172
|
}
|
|
178
173
|
|
|
179
174
|
function applyVisionDefaults(profile) {
|
|
@@ -201,10 +196,10 @@ function applyThinkingDefaults(profile) {
|
|
|
201
196
|
function removeThinkingDefaults(profile) {
|
|
202
197
|
const flags = { ...profile.flags, ...GENERAL_DEFAULTS };
|
|
203
198
|
delete flags.chatTemplateKwargs;
|
|
204
|
-
return applyProfileFlags(profile, flags
|
|
199
|
+
return applyProfileFlags(profile, flags);
|
|
205
200
|
}
|
|
206
201
|
|
|
207
|
-
function applyProfileFlags(profile, flags
|
|
202
|
+
function applyProfileFlags(profile, flags) {
|
|
208
203
|
const next = {
|
|
209
204
|
...profile,
|
|
210
205
|
flags,
|
|
@@ -214,43 +209,9 @@ function applyProfileFlags(profile, flags, edits = {}) {
|
|
|
214
209
|
pi: { ...(profile.harnesses?.pi ?? {}), enabled: true, model: `${profile.providerId ?? profile.backend}/${profile.modelAlias ?? profile.id}` },
|
|
215
210
|
},
|
|
216
211
|
};
|
|
217
|
-
next.commandArgv = updateArgv(profile.commandArgv ?? [], {
|
|
218
|
-
"--host": flags.host,
|
|
219
|
-
"--port": flags.port,
|
|
220
|
-
"--ctx-size": flags.ctxSize,
|
|
221
|
-
"--cache-type-k": flags.cacheTypeK,
|
|
222
|
-
"--cache-type-v": flags.cacheTypeV,
|
|
223
|
-
"--top-k": flags.topK,
|
|
224
|
-
"--presence-penalty": flags.presencePenalty,
|
|
225
|
-
"--repeat-penalty": flags.repeatPenalty,
|
|
226
|
-
...(flags.chatTemplateKwargs ? { "--chat-template-kwargs": JSON.stringify(flags.chatTemplateKwargs) } : {}),
|
|
227
|
-
}, edits);
|
|
228
|
-
return next;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function updateArgv(argv, values, edits = {}) {
|
|
232
|
-
let next = [...argv];
|
|
233
|
-
for (const flag of edits.remove ?? []) next = removeOption(next, flag);
|
|
234
|
-
for (const [flag, value] of Object.entries({ ...values, ...(edits.values ?? {}) })) {
|
|
235
|
-
if (value === undefined) continue;
|
|
236
|
-
const index = next.indexOf(flag);
|
|
237
|
-
if (index === -1) next.push(flag, String(value));
|
|
238
|
-
else next[index + 1] = String(value);
|
|
239
|
-
}
|
|
240
212
|
return next;
|
|
241
213
|
}
|
|
242
214
|
|
|
243
|
-
function removeOption(argv, flag) {
|
|
244
|
-
const next = [];
|
|
245
|
-
for (let i = 0; i < argv.length; i++) {
|
|
246
|
-
if (argv[i] === flag) {
|
|
247
|
-
if (argv[i + 1] && !argv[i + 1].startsWith("--")) i += 1;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
next.push(argv[i]);
|
|
251
|
-
}
|
|
252
|
-
return next;
|
|
253
|
-
}
|
|
254
215
|
|
|
255
216
|
function renderMemoryEstimate(profile) {
|
|
256
217
|
try {
|
|
@@ -283,18 +244,6 @@ async function runtimeSupportsGemma4Unified() {
|
|
|
283
244
|
}
|
|
284
245
|
}
|
|
285
246
|
|
|
286
|
-
function detectionSummary(caps) {
|
|
287
|
-
const parts = [];
|
|
288
|
-
if (caps.architecture) parts.push(caps.architecture);
|
|
289
|
-
if (caps.quant) parts.push(caps.quant);
|
|
290
|
-
if (caps.mtp) parts.push("MTP");
|
|
291
|
-
if (caps.qat) parts.push("QAT");
|
|
292
|
-
|
|
293
|
-
if (caps.thinking) parts.push("thinking");
|
|
294
|
-
if (caps.vision) parts.push("vision");
|
|
295
|
-
return parts.length > 0 ? parts.join(" · ") : "standard GGUF";
|
|
296
|
-
}
|
|
297
|
-
|
|
298
247
|
function samplingSummary(flags) {
|
|
299
248
|
return `temp ${flags.temperature}, top-p ${flags.topP}, top-k ${flags.topK}`;
|
|
300
249
|
}
|
|
@@ -342,7 +291,7 @@ export async function configureMlxProfile(prompt, profile) {
|
|
|
342
291
|
["Backend", configured.backend],
|
|
343
292
|
["Endpoint", configured.baseUrl],
|
|
344
293
|
["Context", String(configured.flags.ctxSize) + " tokens"],
|
|
345
|
-
["Thinking", configured.capabilities
|
|
294
|
+
["Thinking", configured.capabilities?.thinking ? "on" : "off"],
|
|
346
295
|
["Vision", configured.capabilities.vision ? "yes" : "no"],
|
|
347
296
|
])));
|
|
348
297
|
|
|
@@ -352,33 +301,19 @@ export async function configureMlxProfile(prompt, profile) {
|
|
|
352
301
|
|
|
353
302
|
async function applyMlxThinkingToggle(profile, enabled) {
|
|
354
303
|
if (!profile.capabilities.thinking) return profile;
|
|
355
|
-
const { computeMlxVlmFlags } = await import("./mlx-flags.mjs");
|
|
356
|
-
const { args } = computeMlxVlmFlags(profile.modelPath, {
|
|
357
|
-
port: profile.flags.port,
|
|
358
|
-
ctxSize: profile.flags.ctxSize,
|
|
359
|
-
thinkingEnabled: enabled,
|
|
360
|
-
});
|
|
361
304
|
return {
|
|
362
305
|
...profile,
|
|
363
|
-
commandArgv: args,
|
|
364
306
|
capabilities: { ...profile.capabilities, thinkingEnabled: enabled },
|
|
365
307
|
};
|
|
366
308
|
}
|
|
367
309
|
|
|
368
310
|
function applyMlxContextSize(profile, ctxSize) {
|
|
369
311
|
const flags = { ...profile.flags, ctxSize };
|
|
370
|
-
|
|
312
|
+
return {
|
|
371
313
|
...profile,
|
|
372
314
|
flags,
|
|
373
315
|
baseUrl: baseUrlForFlags(flags),
|
|
374
316
|
};
|
|
375
|
-
const idx = next.commandArgv.indexOf("--max-kv-size");
|
|
376
|
-
if (idx !== -1 && next.commandArgv[idx + 1] != null) {
|
|
377
|
-
next.commandArgv[idx + 1] = String(ctxSize);
|
|
378
|
-
} else if (ctxSize && ctxSize > 0) {
|
|
379
|
-
next.commandArgv.push("--max-kv-size", String(ctxSize));
|
|
380
|
-
}
|
|
381
|
-
return next;
|
|
382
317
|
}
|
|
383
318
|
|
|
384
319
|
function renderMlxMemoryEstimate(profile) {
|
package/src/profiles.mjs
CHANGED
|
@@ -18,10 +18,6 @@ export function profileJsonPath(id) {
|
|
|
18
18
|
return join(profileDir(id), "profile.json");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function commandJsonPath(id) {
|
|
22
|
-
return join(profileDir(id), "command.json");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
export function notesPath(id) {
|
|
26
22
|
return join(profileDir(id), "notes.md");
|
|
27
23
|
}
|
|
@@ -69,9 +65,8 @@ export async function saveProfile(profile, options = {}) {
|
|
|
69
65
|
};
|
|
70
66
|
await writeJson(profileJsonPath(id), saved);
|
|
71
67
|
|
|
72
|
-
// Note:
|
|
73
|
-
// fresh from
|
|
74
|
-
// process.mjs). commandArgv is kept in the profile for backwards compat.
|
|
68
|
+
// Note: commandArgv is no longer stored — computeServerCommand computes
|
|
69
|
+
// the full command fresh from profile config at launch time.
|
|
75
70
|
|
|
76
71
|
if (!existsSync(notesPath(id))) {
|
|
77
72
|
await writeFile(notesPath(id), `# ${saved.label}\n\nNotes for this model profile.\n`, "utf8");
|
|
@@ -134,7 +129,7 @@ export async function createProfileFromModel(model, backendId, drafterPath) {
|
|
|
134
129
|
// If a drafter is provided, this model supports MTP regardless of filename
|
|
135
130
|
const hasMtp = caps.mtp || Boolean(drafterPath);
|
|
136
131
|
const backend = backendId ?? (hasMtp ? "llama-cpp-mtp" : "llama-cpp");
|
|
137
|
-
const { flags
|
|
132
|
+
const { flags } = computeFlags(
|
|
138
133
|
{ ...caps, mtp: hasMtp },
|
|
139
134
|
model.path,
|
|
140
135
|
model.mmprojPath,
|
|
@@ -154,21 +149,15 @@ export async function createProfileFromModel(model, backendId, drafterPath) {
|
|
|
154
149
|
capabilities: summarizeCapabilities({ ...caps, mtp: hasMtp }),
|
|
155
150
|
preset: null, // no presets — auto-detected
|
|
156
151
|
flags,
|
|
157
|
-
commandArgv: argv,
|
|
158
152
|
});
|
|
159
153
|
}
|
|
160
154
|
|
|
161
155
|
// ── Auto-create profile from a discovered MLX model ────────────────────────
|
|
162
156
|
|
|
163
157
|
export async function createProfileFromMlxModel(model) {
|
|
164
|
-
const {
|
|
158
|
+
const { DEFAULT_PORT } = await import("./mlx-flags.mjs");
|
|
165
159
|
const caps = await detectMlxCapabilities(model.filePath);
|
|
166
160
|
const ctxSize = defaultMlxContextLength(caps.contextLength, detectHardware().totalRamBytes / (1024 ** 3));
|
|
167
|
-
const { args } = computeMlxVlmFlags(model.filePath, {
|
|
168
|
-
port: DEFAULT_PORT,
|
|
169
|
-
ctxSize,
|
|
170
|
-
thinkingEnabled: caps.thinking,
|
|
171
|
-
});
|
|
172
161
|
return normalizeProfile({
|
|
173
162
|
id: slugFromLabel(model.label),
|
|
174
163
|
label: model.label,
|
|
@@ -182,7 +171,6 @@ export async function createProfileFromMlxModel(model) {
|
|
|
182
171
|
modelSizeBytes: model.sizeBytes,
|
|
183
172
|
capabilities: caps,
|
|
184
173
|
flags: { host: "127.0.0.1", port: DEFAULT_PORT, ctxSize },
|
|
185
|
-
commandArgv: args,
|
|
186
174
|
});
|
|
187
175
|
}
|
|
188
176
|
|
|
@@ -203,11 +191,6 @@ function summarizeCapabilities(caps) {
|
|
|
203
191
|
|
|
204
192
|
// ── State files (for running servers) ──────────────────────────────────────
|
|
205
193
|
|
|
206
|
-
export async function readCommandArgv(profile) {
|
|
207
|
-
const command = await readJson(commandJsonPath(profile.id), null);
|
|
208
|
-
return Array.isArray(command?.argv) ? command.argv.map(String) : (profile.commandArgv ?? []).map(String);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
194
|
export async function readState(id) {
|
|
212
195
|
return readJson(statePath(id), null);
|
|
213
196
|
}
|
package/src/command.mjs
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export function buildPrettyCommand(profile, binary = "llama-server") {
|
|
2
|
-
const argv = profile.commandArgv ?? [];
|
|
3
|
-
const lines = [`${quoteShell(binary)} \\`];
|
|
4
|
-
for (let i = 0; i < argv.length; i++) {
|
|
5
|
-
const arg = argv[i];
|
|
6
|
-
const next = argv[i + 1];
|
|
7
|
-
const hasValue = arg.startsWith("--") && next && !next.startsWith("--");
|
|
8
|
-
if (hasValue) {
|
|
9
|
-
lines.push(` ${arg} ${quoteShell(next)}${i + 2 < argv.length ? " \\" : ""}`);
|
|
10
|
-
i += 1;
|
|
11
|
-
} else {
|
|
12
|
-
lines.push(` ${arg}${i + 1 < argv.length ? " \\" : ""}`);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return lines.join("\n");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function quoteShell(value) {
|
|
19
|
-
const text = String(value);
|
|
20
|
-
return /^[A-Za-z0-9_/@%+=:,.-]+$/u.test(text) ? text : `'${text.replace(/'/gu, `'"'"'`)}'`;
|
|
21
|
-
}
|