@khanglvm/llm-router 2.4.0 → 2.5.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/CHANGELOG.md +22 -0
- package/README.md +12 -0
- package/package.json +2 -1
- package/src/node/huggingface-gguf.js +273 -0
- package/src/node/llamacpp-runtime.js +309 -0
- package/src/node/local-model-browser.js +132 -0
- package/src/node/local-model-capacity.js +39 -0
- package/src/node/local-models-service.js +238 -0
- package/src/node/start-command.js +12 -0
- package/src/node/web-console-client.js +27 -27
- package/src/node/web-console-server.js +575 -0
- package/src/node/web-console-styles.generated.js +1 -1
- package/src/node/web-console-ui/api-client.js +94 -0
- package/src/node/web-console-ui/local-models-utils.js +138 -0
- package/src/runtime/config.js +22 -7
- package/src/runtime/handler/provider-translation.js +5 -5
- package/src/runtime/local-models.js +168 -0
- package/src/translator/response/openai-to-claude.js +70 -9
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -61,6 +62,27 @@ import {
|
|
|
61
62
|
} from "./ollama-client.js";
|
|
62
63
|
import { estimateMaxContext, estimateModelVram, formatBytes } from "./ollama-hardware.js";
|
|
63
64
|
import { detectOllamaInstallation, installOllama, startOllamaServer, stopOllamaServer, isOllamaRunning } from "./ollama-install.js";
|
|
65
|
+
import { browseForLocalModelPath, scanLocalModelPath } from "./local-model-browser.js";
|
|
66
|
+
import {
|
|
67
|
+
detectLlamacppCandidates,
|
|
68
|
+
startConfiguredLlamacppRuntime,
|
|
69
|
+
stopManagedLlamacppRuntime,
|
|
70
|
+
validateLlamacppCommand
|
|
71
|
+
} from "./llamacpp-runtime.js";
|
|
72
|
+
import {
|
|
73
|
+
getManagedLocalModelsDir,
|
|
74
|
+
reconcileLocalModelPaths,
|
|
75
|
+
registerAttachedLlamacppModel,
|
|
76
|
+
registerManagedLlamacppModel,
|
|
77
|
+
removeLocalBaseModel,
|
|
78
|
+
saveLocalModelVariant
|
|
79
|
+
,
|
|
80
|
+
updateLocalBaseModelPath
|
|
81
|
+
} from "./local-models-service.js";
|
|
82
|
+
import {
|
|
83
|
+
downloadManagedHuggingFaceGguf,
|
|
84
|
+
searchHuggingFaceGgufCandidates
|
|
85
|
+
} from "./huggingface-gguf.js";
|
|
64
86
|
import {
|
|
65
87
|
CONFIG_VERSION,
|
|
66
88
|
DEFAULT_MODEL_ALIAS_ID,
|
|
@@ -846,6 +868,80 @@ function routeSnapshotDocument(configState) {
|
|
|
846
868
|
return configState.parseError ? null : (configState.normalizedConfig || buildDefaultConfigObject());
|
|
847
869
|
}
|
|
848
870
|
|
|
871
|
+
function readConfiguredLlamacppRuntime(config = {}) {
|
|
872
|
+
const runtime = config?.metadata?.localModels?.runtime?.llamacpp;
|
|
873
|
+
if (!runtime || typeof runtime !== "object") {
|
|
874
|
+
return {
|
|
875
|
+
selectedCommand: "",
|
|
876
|
+
selectedDirectory: "",
|
|
877
|
+
manualCommand: "",
|
|
878
|
+
host: "127.0.0.1",
|
|
879
|
+
port: 39391,
|
|
880
|
+
startWithRouter: false,
|
|
881
|
+
status: ""
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const selectedCommand = String(runtime.selectedCommand || runtime.manualCommand || runtime.command || "").trim();
|
|
886
|
+
return {
|
|
887
|
+
...runtime,
|
|
888
|
+
selectedCommand,
|
|
889
|
+
selectedDirectory: selectedCommand ? path.dirname(selectedCommand) : "",
|
|
890
|
+
manualCommand: String(runtime.manualCommand || "").trim(),
|
|
891
|
+
host: String(runtime.host || "127.0.0.1").trim() || "127.0.0.1",
|
|
892
|
+
port: Number.isInteger(Number(runtime.port)) ? Number(runtime.port) : 39391,
|
|
893
|
+
startWithRouter: runtime.startWithRouter === true,
|
|
894
|
+
status: String(runtime.status || "").trim()
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function buildLlamacppRuntimePayload(runtime = {}, validation = {}, candidates = []) {
|
|
899
|
+
const selectedCommand = String(runtime.selectedCommand || runtime.manualCommand || runtime.command || "").trim();
|
|
900
|
+
return {
|
|
901
|
+
...runtime,
|
|
902
|
+
selectedCommand,
|
|
903
|
+
selectedDirectory: selectedCommand ? path.dirname(selectedCommand) : "",
|
|
904
|
+
...(validation && typeof validation === "object" ? validation : {}),
|
|
905
|
+
candidates
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function updateLlamacppRuntimeConfig(config = {}, runtimePatch = {}) {
|
|
910
|
+
const nextConfig = JSON.parse(JSON.stringify(config || {}));
|
|
911
|
+
if (!nextConfig.metadata || typeof nextConfig.metadata !== "object" || Array.isArray(nextConfig.metadata)) {
|
|
912
|
+
nextConfig.metadata = {};
|
|
913
|
+
}
|
|
914
|
+
if (!nextConfig.metadata.localModels || typeof nextConfig.metadata.localModels !== "object" || Array.isArray(nextConfig.metadata.localModels)) {
|
|
915
|
+
nextConfig.metadata.localModels = {};
|
|
916
|
+
}
|
|
917
|
+
if (!nextConfig.metadata.localModels.runtime || typeof nextConfig.metadata.localModels.runtime !== "object" || Array.isArray(nextConfig.metadata.localModels.runtime)) {
|
|
918
|
+
nextConfig.metadata.localModels.runtime = {};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const currentRuntime = readConfiguredLlamacppRuntime(nextConfig);
|
|
922
|
+
const nextRuntime = {
|
|
923
|
+
...currentRuntime,
|
|
924
|
+
...runtimePatch
|
|
925
|
+
};
|
|
926
|
+
const {
|
|
927
|
+
selectedDirectory: _selectedDirectory,
|
|
928
|
+
candidates: _candidates,
|
|
929
|
+
...persistedRuntime
|
|
930
|
+
} = nextRuntime;
|
|
931
|
+
const selectedCommand = String(nextRuntime.selectedCommand || nextRuntime.manualCommand || nextRuntime.command || "").trim();
|
|
932
|
+
|
|
933
|
+
nextConfig.metadata.localModels.runtime.llamacpp = {
|
|
934
|
+
...nextConfig.metadata.localModels.runtime.llamacpp,
|
|
935
|
+
...persistedRuntime,
|
|
936
|
+
selectedCommand,
|
|
937
|
+
manualCommand: selectedCommand,
|
|
938
|
+
command: selectedCommand,
|
|
939
|
+
path: selectedCommand,
|
|
940
|
+
status: String(nextRuntime.status || "").trim()
|
|
941
|
+
};
|
|
942
|
+
return nextConfig;
|
|
943
|
+
}
|
|
944
|
+
|
|
849
945
|
export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
850
946
|
const {
|
|
851
947
|
host = "127.0.0.1",
|
|
@@ -884,6 +980,40 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
884
980
|
? deps.waitForRuntimeMatch
|
|
885
981
|
: (startOptions, waitOptions = {}) => waitForRuntimeMatch(startOptions, waitOptions);
|
|
886
982
|
const loginSubscriptionFn = typeof deps.loginSubscription === "function" ? deps.loginSubscription : loginSubscription;
|
|
983
|
+
const getLocalModelSystemInfoFn = typeof deps.getLocalModelSystemInfo === "function"
|
|
984
|
+
? deps.getLocalModelSystemInfo
|
|
985
|
+
: () => ({
|
|
986
|
+
platform: process.platform,
|
|
987
|
+
totalMemoryBytes: os.totalmem(),
|
|
988
|
+
unifiedMemory: process.platform === "darwin"
|
|
989
|
+
});
|
|
990
|
+
const searchHuggingFaceGgufCandidatesFn = typeof deps.searchHuggingFaceGgufCandidates === "function"
|
|
991
|
+
? deps.searchHuggingFaceGgufCandidates
|
|
992
|
+
: searchHuggingFaceGgufCandidates;
|
|
993
|
+
const downloadManagedHuggingFaceGgufFn = typeof deps.downloadManagedHuggingFaceGguf === "function"
|
|
994
|
+
? deps.downloadManagedHuggingFaceGguf
|
|
995
|
+
: (request, runtimeOptions = {}) => downloadManagedHuggingFaceGguf(request, runtimeOptions);
|
|
996
|
+
const localModelPathExistsFn = typeof deps.localModelPathExists === "function"
|
|
997
|
+
? deps.localModelPathExists
|
|
998
|
+
: undefined;
|
|
999
|
+
const browseForLocalModelPathFn = typeof deps.browseForLocalModelPath === "function"
|
|
1000
|
+
? deps.browseForLocalModelPath
|
|
1001
|
+
: browseForLocalModelPath;
|
|
1002
|
+
const scanLocalModelPathFn = typeof deps.scanLocalModelPath === "function"
|
|
1003
|
+
? deps.scanLocalModelPath
|
|
1004
|
+
: scanLocalModelPath;
|
|
1005
|
+
const detectLlamacppCandidatesFn = typeof deps.detectLlamacppCandidates === "function"
|
|
1006
|
+
? deps.detectLlamacppCandidates
|
|
1007
|
+
: detectLlamacppCandidates;
|
|
1008
|
+
const startConfiguredLlamacppRuntimeFn = typeof deps.startConfiguredLlamacppRuntime === "function"
|
|
1009
|
+
? deps.startConfiguredLlamacppRuntime
|
|
1010
|
+
: (config, callbacks = {}, runtimeDeps = {}) => startConfiguredLlamacppRuntime(config, callbacks, runtimeDeps);
|
|
1011
|
+
const stopManagedLlamacppRuntimeFn = typeof deps.stopManagedLlamacppRuntime === "function"
|
|
1012
|
+
? deps.stopManagedLlamacppRuntime
|
|
1013
|
+
: (callbacks = {}) => stopManagedLlamacppRuntime(callbacks);
|
|
1014
|
+
const validateLlamacppCommandFn = typeof deps.validateLlamacppCommand === "function"
|
|
1015
|
+
? deps.validateLlamacppCommand
|
|
1016
|
+
: validateLlamacppCommand;
|
|
887
1017
|
const ampClientEnv = deps.ampClientEnv && typeof deps.ampClientEnv === "object" ? deps.ampClientEnv : process.env;
|
|
888
1018
|
const ampClientCwd = typeof deps.ampClientCwd === "string" && deps.ampClientCwd.trim() ? deps.ampClientCwd : process.cwd();
|
|
889
1019
|
const codexCliEnv = deps.codexCliEnv && typeof deps.codexCliEnv === "object" ? deps.codexCliEnv : process.env;
|
|
@@ -3131,6 +3261,451 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
3131
3261
|
return;
|
|
3132
3262
|
}
|
|
3133
3263
|
|
|
3264
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/attach") {
|
|
3265
|
+
const body = await readJsonBody(req);
|
|
3266
|
+
const id = String(body.id || "").trim();
|
|
3267
|
+
const filePath = String(body.filePath || "").trim();
|
|
3268
|
+
if (!id || !filePath) {
|
|
3269
|
+
sendJson(res, 400, {
|
|
3270
|
+
error: "id and filePath are required."
|
|
3271
|
+
});
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
const configState = await readConfigState(configPath);
|
|
3275
|
+
if (configState.parseError) {
|
|
3276
|
+
sendJson(res, 400, {
|
|
3277
|
+
error: `Config JSON must parse before attaching a local model: ${configState.parseError}`
|
|
3278
|
+
});
|
|
3279
|
+
return;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
const updated = await registerAttachedLlamacppModel(configState.rawConfig || {}, {
|
|
3283
|
+
id,
|
|
3284
|
+
displayName: String(body.displayName || "").trim(),
|
|
3285
|
+
filePath,
|
|
3286
|
+
metadata: body.metadata || {}
|
|
3287
|
+
});
|
|
3288
|
+
const { savedConfig } = await writeAndBroadcastConfig(updated, {
|
|
3289
|
+
source: "local-models-attach"
|
|
3290
|
+
});
|
|
3291
|
+
sendJson(res, 200, {
|
|
3292
|
+
ok: true,
|
|
3293
|
+
library: savedConfig?.metadata?.localModels?.library || {}
|
|
3294
|
+
});
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/locate") {
|
|
3299
|
+
const body = await readJsonBody(req);
|
|
3300
|
+
const baseModelId = String(body.baseModelId || "").trim();
|
|
3301
|
+
const filePath = String(body.filePath || "").trim();
|
|
3302
|
+
if (!baseModelId || !filePath) {
|
|
3303
|
+
sendJson(res, 400, {
|
|
3304
|
+
error: "baseModelId and filePath are required."
|
|
3305
|
+
});
|
|
3306
|
+
return;
|
|
3307
|
+
}
|
|
3308
|
+
const configState = await readConfigState(configPath);
|
|
3309
|
+
if (configState.parseError) {
|
|
3310
|
+
sendJson(res, 400, {
|
|
3311
|
+
error: `Config JSON must parse before locating a local model: ${configState.parseError}`
|
|
3312
|
+
});
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
try {
|
|
3317
|
+
const relocated = await updateLocalBaseModelPath(configState.rawConfig || {}, baseModelId, filePath);
|
|
3318
|
+
const reconciled = await reconcileLocalModelPaths(relocated, {
|
|
3319
|
+
...(localModelPathExistsFn ? { pathExists: localModelPathExistsFn } : {})
|
|
3320
|
+
});
|
|
3321
|
+
const { savedConfig } = await writeAndBroadcastConfig(reconciled, {
|
|
3322
|
+
source: "local-models-locate"
|
|
3323
|
+
});
|
|
3324
|
+
sendJson(res, 200, {
|
|
3325
|
+
ok: true,
|
|
3326
|
+
library: savedConfig?.metadata?.localModels?.library || {},
|
|
3327
|
+
variants: savedConfig?.metadata?.localModels?.variants || {}
|
|
3328
|
+
});
|
|
3329
|
+
} catch (error) {
|
|
3330
|
+
sendJson(res, 400, {
|
|
3331
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/remove") {
|
|
3338
|
+
const body = await readJsonBody(req);
|
|
3339
|
+
const baseModelId = String(body.baseModelId || "").trim();
|
|
3340
|
+
if (!baseModelId) {
|
|
3341
|
+
sendJson(res, 400, {
|
|
3342
|
+
error: "baseModelId is required."
|
|
3343
|
+
});
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
const configState = await readConfigState(configPath);
|
|
3347
|
+
if (configState.parseError) {
|
|
3348
|
+
sendJson(res, 400, {
|
|
3349
|
+
error: `Config JSON must parse before removing a local model: ${configState.parseError}`
|
|
3350
|
+
});
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
const updated = await removeLocalBaseModel(configState.rawConfig || {}, baseModelId);
|
|
3355
|
+
const { savedConfig } = await writeAndBroadcastConfig(updated, {
|
|
3356
|
+
source: "local-models-remove"
|
|
3357
|
+
});
|
|
3358
|
+
sendJson(res, 200, {
|
|
3359
|
+
ok: true,
|
|
3360
|
+
library: savedConfig?.metadata?.localModels?.library || {},
|
|
3361
|
+
variants: savedConfig?.metadata?.localModels?.variants || {}
|
|
3362
|
+
});
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/reconcile") {
|
|
3367
|
+
const configState = await readConfigState(configPath);
|
|
3368
|
+
if (configState.parseError) {
|
|
3369
|
+
sendJson(res, 400, {
|
|
3370
|
+
error: `Config JSON must parse before refreshing local model status: ${configState.parseError}`
|
|
3371
|
+
});
|
|
3372
|
+
return;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
const updated = await reconcileLocalModelPaths(configState.rawConfig || {}, {
|
|
3376
|
+
...(localModelPathExistsFn ? { pathExists: localModelPathExistsFn } : {})
|
|
3377
|
+
});
|
|
3378
|
+
const { savedConfig } = await writeAndBroadcastConfig(updated, {
|
|
3379
|
+
source: "local-models-reconcile"
|
|
3380
|
+
});
|
|
3381
|
+
sendJson(res, 200, {
|
|
3382
|
+
ok: true,
|
|
3383
|
+
library: savedConfig?.metadata?.localModels?.library || {},
|
|
3384
|
+
variants: savedConfig?.metadata?.localModels?.variants || {}
|
|
3385
|
+
});
|
|
3386
|
+
return;
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/runtime/discover") {
|
|
3390
|
+
const configState = await readConfigState(configPath);
|
|
3391
|
+
if (configState.parseError) {
|
|
3392
|
+
sendJson(res, 400, {
|
|
3393
|
+
error: `Config JSON must parse before discovering llama.cpp runtimes: ${configState.parseError}`
|
|
3394
|
+
});
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
const configuredRuntime = readConfiguredLlamacppRuntime(configState.rawConfig || {});
|
|
3399
|
+
const candidates = detectLlamacppCandidatesFn();
|
|
3400
|
+
const hydratedCandidates = candidates.map((candidate) => {
|
|
3401
|
+
const validation = validateLlamacppCommandFn(candidate.path);
|
|
3402
|
+
return {
|
|
3403
|
+
...candidate,
|
|
3404
|
+
directory: path.dirname(candidate.path),
|
|
3405
|
+
...validation,
|
|
3406
|
+
current: candidate.path === configuredRuntime.selectedCommand
|
|
3407
|
+
};
|
|
3408
|
+
});
|
|
3409
|
+
|
|
3410
|
+
sendJson(res, 200, {
|
|
3411
|
+
runtime: buildLlamacppRuntimePayload(configuredRuntime, {}, hydratedCandidates)
|
|
3412
|
+
});
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/runtime/select") {
|
|
3417
|
+
const body = await readJsonBody(req);
|
|
3418
|
+
const command = String(body.command || "").trim();
|
|
3419
|
+
if (!command) {
|
|
3420
|
+
sendJson(res, 400, {
|
|
3421
|
+
error: "command is required."
|
|
3422
|
+
});
|
|
3423
|
+
return;
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
const configState = await readConfigState(configPath);
|
|
3427
|
+
if (configState.parseError) {
|
|
3428
|
+
sendJson(res, 400, {
|
|
3429
|
+
error: `Config JSON must parse before selecting a llama.cpp runtime: ${configState.parseError}`
|
|
3430
|
+
});
|
|
3431
|
+
return;
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
const validation = validateLlamacppCommandFn(command);
|
|
3435
|
+
if (!validation?.ok) {
|
|
3436
|
+
sendJson(res, 400, {
|
|
3437
|
+
error: validation?.errorMessage || `Failed validating llama.cpp runtime '${command}'.`,
|
|
3438
|
+
runtime: buildLlamacppRuntimePayload(readConfiguredLlamacppRuntime(configState.rawConfig || {}), validation)
|
|
3439
|
+
});
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
const updated = updateLlamacppRuntimeConfig(configState.rawConfig || {}, {
|
|
3444
|
+
selectedCommand: command,
|
|
3445
|
+
manualCommand: command,
|
|
3446
|
+
status: "stopped"
|
|
3447
|
+
});
|
|
3448
|
+
const { savedConfig } = await writeAndBroadcastConfig(updated, {
|
|
3449
|
+
source: "local-models-runtime-select"
|
|
3450
|
+
});
|
|
3451
|
+
const configuredRuntime = readConfiguredLlamacppRuntime(savedConfig || {});
|
|
3452
|
+
sendJson(res, 200, {
|
|
3453
|
+
ok: true,
|
|
3454
|
+
runtime: buildLlamacppRuntimePayload(configuredRuntime, {
|
|
3455
|
+
...validation,
|
|
3456
|
+
status: configuredRuntime.status || "stopped"
|
|
3457
|
+
})
|
|
3458
|
+
});
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/runtime/settings") {
|
|
3463
|
+
const body = await readJsonBody(req);
|
|
3464
|
+
const configState = await readConfigState(configPath);
|
|
3465
|
+
if (configState.parseError) {
|
|
3466
|
+
sendJson(res, 400, {
|
|
3467
|
+
error: `Config JSON must parse before updating llama.cpp runtime settings: ${configState.parseError}`
|
|
3468
|
+
});
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
const updated = updateLlamacppRuntimeConfig(configState.rawConfig || {}, {
|
|
3473
|
+
...(body?.startWithRouter !== undefined ? { startWithRouter: body.startWithRouter === true } : {})
|
|
3474
|
+
});
|
|
3475
|
+
const { savedConfig } = await writeAndBroadcastConfig(updated, {
|
|
3476
|
+
source: "local-models-runtime-settings"
|
|
3477
|
+
});
|
|
3478
|
+
sendJson(res, 200, {
|
|
3479
|
+
ok: true,
|
|
3480
|
+
runtime: buildLlamacppRuntimePayload(readConfiguredLlamacppRuntime(savedConfig || {}))
|
|
3481
|
+
});
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/runtime/start") {
|
|
3486
|
+
const configState = await readConfigState(configPath);
|
|
3487
|
+
if (configState.parseError) {
|
|
3488
|
+
sendJson(res, 400, {
|
|
3489
|
+
error: `Config JSON must parse before starting llama.cpp runtime: ${configState.parseError}`
|
|
3490
|
+
});
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
const configuredRuntime = readConfiguredLlamacppRuntime(configState.rawConfig || {});
|
|
3495
|
+
if (!configuredRuntime.selectedCommand) {
|
|
3496
|
+
sendJson(res, 400, {
|
|
3497
|
+
error: "Select a llama.cpp runtime before starting it."
|
|
3498
|
+
});
|
|
3499
|
+
return;
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
const listenerPids = await Promise.resolve(listListeningPidsFn(configuredRuntime.port)).catch(() => []);
|
|
3503
|
+
let validation = validateLlamacppCommandFn(configuredRuntime.selectedCommand);
|
|
3504
|
+
if ((listenerPids || []).length === 0) {
|
|
3505
|
+
const started = await startConfiguredLlamacppRuntimeFn(configState.rawConfig || {}, {
|
|
3506
|
+
line: (message) => addLog("info", message),
|
|
3507
|
+
error: (message) => addLog("warn", message)
|
|
3508
|
+
});
|
|
3509
|
+
if (!started?.ok) {
|
|
3510
|
+
sendJson(res, 502, {
|
|
3511
|
+
error: started?.errorMessage || "Failed to start llama.cpp runtime."
|
|
3512
|
+
});
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
validation = started?.validation || validation;
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
const updated = updateLlamacppRuntimeConfig(configState.rawConfig || {}, {
|
|
3519
|
+
status: "running"
|
|
3520
|
+
});
|
|
3521
|
+
const { savedConfig } = await writeAndBroadcastConfig(updated, {
|
|
3522
|
+
source: "local-models-runtime-start"
|
|
3523
|
+
});
|
|
3524
|
+
sendJson(res, 200, {
|
|
3525
|
+
ok: true,
|
|
3526
|
+
runtime: buildLlamacppRuntimePayload(readConfiguredLlamacppRuntime(savedConfig || {}), {
|
|
3527
|
+
...(validation && typeof validation === "object" ? validation : {}),
|
|
3528
|
+
status: "running"
|
|
3529
|
+
})
|
|
3530
|
+
});
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/runtime/stop") {
|
|
3535
|
+
const configState = await readConfigState(configPath);
|
|
3536
|
+
if (configState.parseError) {
|
|
3537
|
+
sendJson(res, 400, {
|
|
3538
|
+
error: `Config JSON must parse before stopping llama.cpp runtime: ${configState.parseError}`
|
|
3539
|
+
});
|
|
3540
|
+
return;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
const configuredRuntime = readConfiguredLlamacppRuntime(configState.rawConfig || {});
|
|
3544
|
+
await stopManagedLlamacppRuntimeFn({
|
|
3545
|
+
line: (message) => addLog("info", message),
|
|
3546
|
+
error: (message) => addLog("warn", message)
|
|
3547
|
+
});
|
|
3548
|
+
const listenerPids = await Promise.resolve(listListeningPidsFn(configuredRuntime.port)).catch(() => []);
|
|
3549
|
+
for (const pid of listenerPids) {
|
|
3550
|
+
if (!Number.isInteger(Number(pid))) continue;
|
|
3551
|
+
await stopProcessByPidFn(Number(pid)).catch(() => {});
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
const updated = updateLlamacppRuntimeConfig(configState.rawConfig || {}, {
|
|
3555
|
+
status: "stopped"
|
|
3556
|
+
});
|
|
3557
|
+
const { savedConfig } = await writeAndBroadcastConfig(updated, {
|
|
3558
|
+
source: "local-models-runtime-stop"
|
|
3559
|
+
});
|
|
3560
|
+
sendJson(res, 200, {
|
|
3561
|
+
ok: true,
|
|
3562
|
+
runtime: buildLlamacppRuntimePayload(readConfiguredLlamacppRuntime(savedConfig || {}), {
|
|
3563
|
+
status: "stopped"
|
|
3564
|
+
})
|
|
3565
|
+
});
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/browse") {
|
|
3570
|
+
const body = await readJsonBody(req);
|
|
3571
|
+
const selection = String(body.selection || "file").trim() || "file";
|
|
3572
|
+
try {
|
|
3573
|
+
const picked = await browseForLocalModelPathFn({ selection });
|
|
3574
|
+
const matches = picked?.canceled || !picked?.path || selection === "runtime"
|
|
3575
|
+
? []
|
|
3576
|
+
: await scanLocalModelPathFn(picked.path);
|
|
3577
|
+
sendJson(res, 200, {
|
|
3578
|
+
ok: true,
|
|
3579
|
+
selection: picked,
|
|
3580
|
+
matches
|
|
3581
|
+
});
|
|
3582
|
+
} catch (error) {
|
|
3583
|
+
sendJson(res, 500, {
|
|
3584
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/scan-path") {
|
|
3591
|
+
const body = await readJsonBody(req);
|
|
3592
|
+
const targetPath = String(body.path || "").trim();
|
|
3593
|
+
if (!targetPath) {
|
|
3594
|
+
sendJson(res, 400, { error: "path is required." });
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
try {
|
|
3599
|
+
const matches = await scanLocalModelPathFn(targetPath);
|
|
3600
|
+
sendJson(res, 200, {
|
|
3601
|
+
ok: true,
|
|
3602
|
+
path: targetPath,
|
|
3603
|
+
matches
|
|
3604
|
+
});
|
|
3605
|
+
} catch (error) {
|
|
3606
|
+
sendJson(res, 400, {
|
|
3607
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/search-huggingface") {
|
|
3614
|
+
const body = await readJsonBody(req);
|
|
3615
|
+
const results = await searchHuggingFaceGgufCandidatesFn(String(body.query || "").trim(), {
|
|
3616
|
+
limit: body.limit,
|
|
3617
|
+
totalMemoryBytes: os.totalmem(),
|
|
3618
|
+
expectedContextWindow: Number.isInteger(Number(body.expectedContextWindow))
|
|
3619
|
+
? Number(body.expectedContextWindow)
|
|
3620
|
+
: 200000
|
|
3621
|
+
});
|
|
3622
|
+
sendJson(res, 200, { results });
|
|
3623
|
+
return;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/download-managed") {
|
|
3627
|
+
const body = await readJsonBody(req);
|
|
3628
|
+
const id = String(body.id || "").trim();
|
|
3629
|
+
const repo = String(body.repo || "").trim();
|
|
3630
|
+
const file = String(body.file || "").trim();
|
|
3631
|
+
if (!id || !repo || !file) {
|
|
3632
|
+
sendJson(res, 400, { error: "id, repo, and file are required." });
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
startJsonLineStream(res);
|
|
3637
|
+
writeJsonLine(res, { type: "start", id, repo, file });
|
|
3638
|
+
|
|
3639
|
+
try {
|
|
3640
|
+
const destinationPath = path.join(getManagedLocalModelsDir(), repo, file);
|
|
3641
|
+
const downloaded = await downloadManagedHuggingFaceGgufFn({
|
|
3642
|
+
id,
|
|
3643
|
+
displayName: String(body.displayName || "").trim() || file,
|
|
3644
|
+
repo,
|
|
3645
|
+
file,
|
|
3646
|
+
destinationPath
|
|
3647
|
+
}, {
|
|
3648
|
+
onProgress: (event) => writeJsonLine(res, { type: "progress", event })
|
|
3649
|
+
});
|
|
3650
|
+
const configState = await readConfigState(configPath);
|
|
3651
|
+
const updated = await registerManagedLlamacppModel(configState.rawConfig || {}, {
|
|
3652
|
+
id,
|
|
3653
|
+
displayName: String(body.displayName || "").trim() || file,
|
|
3654
|
+
filePath: downloaded.filePath,
|
|
3655
|
+
repo,
|
|
3656
|
+
file,
|
|
3657
|
+
sizeBytes: downloaded.sizeBytes
|
|
3658
|
+
});
|
|
3659
|
+
const { savedConfig } = await writeAndBroadcastConfig(updated, {
|
|
3660
|
+
source: "local-models-download-managed"
|
|
3661
|
+
});
|
|
3662
|
+
writeJsonLine(res, {
|
|
3663
|
+
type: "result",
|
|
3664
|
+
result: {
|
|
3665
|
+
library: savedConfig?.metadata?.localModels?.library || {}
|
|
3666
|
+
}
|
|
3667
|
+
});
|
|
3668
|
+
} catch (error) {
|
|
3669
|
+
writeJsonLine(res, {
|
|
3670
|
+
type: "error",
|
|
3671
|
+
statusCode: Number(error?.statusCode) || 500,
|
|
3672
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3673
|
+
});
|
|
3674
|
+
} finally {
|
|
3675
|
+
res.end();
|
|
3676
|
+
}
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
if (method === "POST" && requestUrl.pathname === "/api/local-models/variants/save") {
|
|
3681
|
+
const body = await readJsonBody(req);
|
|
3682
|
+
const configState = await readConfigState(configPath);
|
|
3683
|
+
if (configState.parseError) {
|
|
3684
|
+
sendJson(res, 400, {
|
|
3685
|
+
error: `Config JSON must parse before saving a local variant: ${configState.parseError}`
|
|
3686
|
+
});
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
try {
|
|
3691
|
+
const updated = await saveLocalModelVariant(configState.rawConfig || {}, body.variant || {}, {
|
|
3692
|
+
system: getLocalModelSystemInfoFn()
|
|
3693
|
+
});
|
|
3694
|
+
const { savedConfig } = await writeAndBroadcastConfig(updated, {
|
|
3695
|
+
source: "local-models-variant-save"
|
|
3696
|
+
});
|
|
3697
|
+
sendJson(res, 200, {
|
|
3698
|
+
ok: true,
|
|
3699
|
+
variants: savedConfig?.metadata?.localModels?.variants || {}
|
|
3700
|
+
});
|
|
3701
|
+
} catch (error) {
|
|
3702
|
+
sendJson(res, 400, {
|
|
3703
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3134
3709
|
if (method === "POST" && requestUrl.pathname === "/api/amp/apply") {
|
|
3135
3710
|
const body = await readJsonBody(req);
|
|
3136
3711
|
let parsed;
|