@tiens.nguyen/gonext-local-worker 1.0.45 → 1.0.47
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/gonext-local-worker.mjs +100 -20
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - `gonext-local-worker simulate-chat [text]` — claim next chat job, push fake reply like the real worker (needs GONEXT_* env)
|
|
8
8
|
* - `gonext-local-worker` — starts polling loop (claims jobs and runs models)
|
|
9
9
|
*/
|
|
10
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
11
11
|
import { execFile as execFileCallback } from "node:child_process";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import { homedir, platform, tmpdir } from "node:os";
|
|
@@ -814,6 +814,26 @@ function sourceLabelFromBase(base) {
|
|
|
814
814
|
}
|
|
815
815
|
}
|
|
816
816
|
|
|
817
|
+
function expandHomePath(rawPath) {
|
|
818
|
+
const v = String(rawPath ?? "").trim();
|
|
819
|
+
if (!v) return "";
|
|
820
|
+
return v.replace(/^~(?=\/)/, homedir());
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function checkModelPathExists(rawPath) {
|
|
824
|
+
const inputPath = String(rawPath ?? "").trim();
|
|
825
|
+
const resolvedPath = expandHomePath(inputPath);
|
|
826
|
+
if (!resolvedPath) {
|
|
827
|
+
return { path: inputPath, resolvedPath: "", exists: false };
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const st = await stat(resolvedPath);
|
|
831
|
+
return { path: inputPath, resolvedPath, exists: st.isDirectory() || st.isFile() };
|
|
832
|
+
} catch {
|
|
833
|
+
return { path: inputPath, resolvedPath, exists: false };
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
817
837
|
/** Log assistant text to stdout; cap size so huge replies do not flood the terminal. */
|
|
818
838
|
function logModelResponseToWorker(jobId, modelId, text) {
|
|
819
839
|
const max = 12000;
|
|
@@ -855,20 +875,26 @@ async function checkOpenAiModels(base, apiKey, source) {
|
|
|
855
875
|
typeof source?.workerHostId === "string" ? source.workerHostId.trim() : "";
|
|
856
876
|
const hostSourceLabel =
|
|
857
877
|
typeof source?.hostName === "string" ? source.hostName.trim() : "";
|
|
878
|
+
const sourcePort = typeof source?.port === "number" ? source.port : null;
|
|
858
879
|
try {
|
|
859
880
|
const res = await fetch(endpoint, { method: "GET", headers });
|
|
860
881
|
if (!res.ok) return { online: false, endpoint, models: [] };
|
|
861
882
|
const j = await res.json();
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
883
|
+
const hostSuffix = hostSourceId ? `@@host:${hostSourceId}` : "";
|
|
884
|
+
const portSuffix = sourcePort ? `@@port:${sourcePort}` : "";
|
|
885
|
+
const modelSuffix = `${hostSuffix}${portSuffix}`;
|
|
886
|
+
const portLabel = sourcePort ? `:${sourcePort}` : "";
|
|
887
|
+
const displaySuffix = hostSourceLabel ? ` (${hostSourceLabel}${portLabel})` : portLabel ? ` (${portLabel})` : "";
|
|
888
|
+
let rawIds = (j.data ?? []).map((d) => d.id).filter(Boolean);
|
|
889
|
+
// mlx_lm.server registers both the local path and the HF model ID.
|
|
890
|
+
// Prefer local absolute paths so each server shows exactly one model entry.
|
|
891
|
+
const localPaths = rawIds.filter((id) => id.startsWith("/") || id.startsWith("~"));
|
|
892
|
+
if (localPaths.length > 0) rawIds = localPaths;
|
|
893
|
+
const models = rawIds.map((id) => ({
|
|
894
|
+
id: hostSourceId ? `${id}@@${hostSourceId}` : id,
|
|
895
|
+
name: `${id}${displaySuffix}`,
|
|
896
|
+
value: `mlx:${id}${modelSuffix}`,
|
|
897
|
+
}));
|
|
872
898
|
return { online: true, endpoint, models };
|
|
873
899
|
} catch {
|
|
874
900
|
return { online: false, endpoint, models: [] };
|
|
@@ -925,8 +951,11 @@ async function runLocalHealthJob(job) {
|
|
|
925
951
|
const ollamaPayloadCount = Array.isArray(payload?.ollamaBaseUrls)
|
|
926
952
|
? payload.ollamaBaseUrls.length
|
|
927
953
|
: 0;
|
|
954
|
+
const mlxUrlCount = Array.isArray(payload?.mlxOpenAiBaseUrls)
|
|
955
|
+
? payload.mlxOpenAiBaseUrls.length
|
|
956
|
+
: payload?.mlxOpenAiBaseUrl ? 1 : 0;
|
|
928
957
|
console.log(
|
|
929
|
-
`[gonext-worker] local_health ${jobId} start (ollamaUrls=${ollamaPayloadCount},
|
|
958
|
+
`[gonext-worker] local_health ${jobId} start (ollamaUrls=${ollamaPayloadCount}, mlxUrls=${mlxUrlCount})`
|
|
930
959
|
);
|
|
931
960
|
const runRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
932
961
|
method: "PATCH",
|
|
@@ -960,7 +989,13 @@ async function runLocalHealthJob(job) {
|
|
|
960
989
|
if (!dedup.has(m.value)) dedup.set(m.value, m);
|
|
961
990
|
}
|
|
962
991
|
}
|
|
963
|
-
|
|
992
|
+
// Support both single mlxOpenAiBaseUrl and multi-port mlxOpenAiBaseUrls
|
|
993
|
+
const mlxRootSingle = normalizeOpenAiV1Root(payload?.mlxOpenAiBaseUrl);
|
|
994
|
+
const mlxRootsMulti = Array.isArray(payload?.mlxOpenAiBaseUrls)
|
|
995
|
+
? payload.mlxOpenAiBaseUrls.map(normalizeOpenAiV1Root).filter(Boolean)
|
|
996
|
+
: [];
|
|
997
|
+
const mlxRoots = mlxRootsMulti.length > 0 ? mlxRootsMulti : (mlxRootSingle ? [mlxRootSingle] : []);
|
|
998
|
+
const mlxRoot = mlxRoots[0] || null;
|
|
964
999
|
const targetWorkerHostId =
|
|
965
1000
|
typeof payload?.targetWorkerHostId === "string"
|
|
966
1001
|
? payload.targetWorkerHostId.trim()
|
|
@@ -969,20 +1004,52 @@ async function runLocalHealthJob(job) {
|
|
|
969
1004
|
typeof payload?.targetWorkerHostName === "string"
|
|
970
1005
|
? payload.targetWorkerHostName.trim()
|
|
971
1006
|
: "";
|
|
972
|
-
let mlxHttp = null;
|
|
973
1007
|
let mlxNative = null;
|
|
1008
|
+
const modelPathChecksRaw = Array.isArray(payload?.modelPathChecks)
|
|
1009
|
+
? payload.modelPathChecks
|
|
1010
|
+
: [];
|
|
1011
|
+
const modelPathChecks = [...new Set(modelPathChecksRaw)]
|
|
1012
|
+
.filter((v) => typeof v === "string" && v.trim())
|
|
1013
|
+
.map((v) => String(v).trim())
|
|
1014
|
+
.slice(0, 8);
|
|
1015
|
+
const modelPathStatus = [];
|
|
974
1016
|
|
|
975
|
-
|
|
1017
|
+
// Collect results from all MLX endpoints (one per port)
|
|
1018
|
+
const mlxHttpResults = [];
|
|
1019
|
+
for (const root of mlxRoots) {
|
|
1020
|
+
// Extract port from URL for model value encoding
|
|
1021
|
+
let port = null;
|
|
1022
|
+
try {
|
|
1023
|
+
const u = new URL(root.replace(/\/v1\/?$/i, ""));
|
|
1024
|
+
const p = parseInt(u.port, 10);
|
|
1025
|
+
if (p > 0) port = p;
|
|
1026
|
+
} catch { /* ignore */ }
|
|
976
1027
|
const mlxStart = Date.now();
|
|
977
|
-
console.log(`[gonext-worker] local_health ${jobId} check mlx HTTP ${
|
|
978
|
-
|
|
1028
|
+
console.log(`[gonext-worker] local_health ${jobId} check mlx HTTP ${root} port=${port ?? "default"}`);
|
|
1029
|
+
const result = await checkOpenAiModels(root, payload?.mlxApiKey ?? "", {
|
|
979
1030
|
workerHostId: targetWorkerHostId,
|
|
980
|
-
hostName: targetWorkerHostName || sourceLabelFromBase(
|
|
1031
|
+
hostName: targetWorkerHostName || sourceLabelFromBase(root),
|
|
1032
|
+
port,
|
|
981
1033
|
});
|
|
982
1034
|
console.log(
|
|
983
|
-
`[gonext-worker] local_health ${jobId} mlx HTTP online=${
|
|
1035
|
+
`[gonext-worker] local_health ${jobId} mlx HTTP online=${result.online} models=${result.models.length} took=${((Date.now() - mlxStart) / 1000).toFixed(2)}s`
|
|
984
1036
|
);
|
|
1037
|
+
mlxHttpResults.push(result);
|
|
985
1038
|
}
|
|
1039
|
+
// Merge all per-port results
|
|
1040
|
+
const mlxModelDedup = new Map();
|
|
1041
|
+
let mlxHttpOnline = false;
|
|
1042
|
+
for (const r of mlxHttpResults) {
|
|
1043
|
+
if (r.online) mlxHttpOnline = true;
|
|
1044
|
+
for (const m of r.models) {
|
|
1045
|
+
if (!mlxModelDedup.has(m.value)) mlxModelDedup.set(m.value, m);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const mlxHttp = mlxRoots.length > 0 ? {
|
|
1049
|
+
online: mlxHttpOnline,
|
|
1050
|
+
endpoint: mlxHttpResults[0]?.endpoint,
|
|
1051
|
+
models: [...mlxModelDedup.values()],
|
|
1052
|
+
} : null;
|
|
986
1053
|
|
|
987
1054
|
const wantNativeFallback =
|
|
988
1055
|
mlxRoot &&
|
|
@@ -1001,6 +1068,11 @@ async function runLocalHealthJob(job) {
|
|
|
1001
1068
|
);
|
|
1002
1069
|
}
|
|
1003
1070
|
|
|
1071
|
+
for (const rawPath of modelPathChecks) {
|
|
1072
|
+
const check = await checkModelPathExists(rawPath);
|
|
1073
|
+
modelPathStatus.push(check);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1004
1076
|
let mlx = null;
|
|
1005
1077
|
if (mlxRoot || mlxNative?.available) {
|
|
1006
1078
|
const httpOk = Boolean(mlxHttp?.online && (mlxHttp?.models?.length ?? 0) > 0);
|
|
@@ -1055,6 +1127,12 @@ async function runLocalHealthJob(job) {
|
|
|
1055
1127
|
}
|
|
1056
1128
|
: undefined,
|
|
1057
1129
|
mlx,
|
|
1130
|
+
setup:
|
|
1131
|
+
modelPathStatus.length > 0
|
|
1132
|
+
? {
|
|
1133
|
+
modelPaths: modelPathStatus,
|
|
1134
|
+
}
|
|
1135
|
+
: undefined,
|
|
1058
1136
|
};
|
|
1059
1137
|
const totalTimeSeconds = (Date.now() - start) / 1000;
|
|
1060
1138
|
const doneRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
@@ -1133,7 +1211,9 @@ async function pollOnce() {
|
|
|
1133
1211
|
const isLocalHealthByType = job.jobType === "local_health";
|
|
1134
1212
|
const isLocalHealthByModelKey = job.modelKey === "local_health";
|
|
1135
1213
|
const isLocalHealthByPayload =
|
|
1136
|
-
Array.isArray(job.payload?.ollamaBaseUrls) ||
|
|
1214
|
+
Array.isArray(job.payload?.ollamaBaseUrls) ||
|
|
1215
|
+
!!job.payload?.mlxOpenAiBaseUrl ||
|
|
1216
|
+
Array.isArray(job.payload?.mlxOpenAiBaseUrls);
|
|
1137
1217
|
if (isLocalHealthByType || isLocalHealthByModelKey || isLocalHealthByPayload) {
|
|
1138
1218
|
const task = runLocalHealthJob(job).catch((e) => {
|
|
1139
1219
|
console.error("[gonext-worker] local_health task error:", e);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gonext-local-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.47",
|
|
4
4
|
"description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|