@tiens.nguyen/gonext-local-worker 1.0.45 → 1.0.46
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 +90 -12
- 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,12 +875,16 @@ 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
|
|
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})` : "";
|
|
864
888
|
const models = (j.data ?? [])
|
|
865
889
|
.map((d) => d.id)
|
|
866
890
|
.filter(Boolean)
|
|
@@ -925,8 +949,11 @@ async function runLocalHealthJob(job) {
|
|
|
925
949
|
const ollamaPayloadCount = Array.isArray(payload?.ollamaBaseUrls)
|
|
926
950
|
? payload.ollamaBaseUrls.length
|
|
927
951
|
: 0;
|
|
952
|
+
const mlxUrlCount = Array.isArray(payload?.mlxOpenAiBaseUrls)
|
|
953
|
+
? payload.mlxOpenAiBaseUrls.length
|
|
954
|
+
: payload?.mlxOpenAiBaseUrl ? 1 : 0;
|
|
928
955
|
console.log(
|
|
929
|
-
`[gonext-worker] local_health ${jobId} start (ollamaUrls=${ollamaPayloadCount},
|
|
956
|
+
`[gonext-worker] local_health ${jobId} start (ollamaUrls=${ollamaPayloadCount}, mlxUrls=${mlxUrlCount})`
|
|
930
957
|
);
|
|
931
958
|
const runRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
932
959
|
method: "PATCH",
|
|
@@ -960,7 +987,13 @@ async function runLocalHealthJob(job) {
|
|
|
960
987
|
if (!dedup.has(m.value)) dedup.set(m.value, m);
|
|
961
988
|
}
|
|
962
989
|
}
|
|
963
|
-
|
|
990
|
+
// Support both single mlxOpenAiBaseUrl and multi-port mlxOpenAiBaseUrls
|
|
991
|
+
const mlxRootSingle = normalizeOpenAiV1Root(payload?.mlxOpenAiBaseUrl);
|
|
992
|
+
const mlxRootsMulti = Array.isArray(payload?.mlxOpenAiBaseUrls)
|
|
993
|
+
? payload.mlxOpenAiBaseUrls.map(normalizeOpenAiV1Root).filter(Boolean)
|
|
994
|
+
: [];
|
|
995
|
+
const mlxRoots = mlxRootsMulti.length > 0 ? mlxRootsMulti : (mlxRootSingle ? [mlxRootSingle] : []);
|
|
996
|
+
const mlxRoot = mlxRoots[0] || null;
|
|
964
997
|
const targetWorkerHostId =
|
|
965
998
|
typeof payload?.targetWorkerHostId === "string"
|
|
966
999
|
? payload.targetWorkerHostId.trim()
|
|
@@ -969,20 +1002,52 @@ async function runLocalHealthJob(job) {
|
|
|
969
1002
|
typeof payload?.targetWorkerHostName === "string"
|
|
970
1003
|
? payload.targetWorkerHostName.trim()
|
|
971
1004
|
: "";
|
|
972
|
-
let mlxHttp = null;
|
|
973
1005
|
let mlxNative = null;
|
|
1006
|
+
const modelPathChecksRaw = Array.isArray(payload?.modelPathChecks)
|
|
1007
|
+
? payload.modelPathChecks
|
|
1008
|
+
: [];
|
|
1009
|
+
const modelPathChecks = [...new Set(modelPathChecksRaw)]
|
|
1010
|
+
.filter((v) => typeof v === "string" && v.trim())
|
|
1011
|
+
.map((v) => String(v).trim())
|
|
1012
|
+
.slice(0, 8);
|
|
1013
|
+
const modelPathStatus = [];
|
|
974
1014
|
|
|
975
|
-
|
|
1015
|
+
// Collect results from all MLX endpoints (one per port)
|
|
1016
|
+
const mlxHttpResults = [];
|
|
1017
|
+
for (const root of mlxRoots) {
|
|
1018
|
+
// Extract port from URL for model value encoding
|
|
1019
|
+
let port = null;
|
|
1020
|
+
try {
|
|
1021
|
+
const u = new URL(root.replace(/\/v1\/?$/i, ""));
|
|
1022
|
+
const p = parseInt(u.port, 10);
|
|
1023
|
+
if (p > 0) port = p;
|
|
1024
|
+
} catch { /* ignore */ }
|
|
976
1025
|
const mlxStart = Date.now();
|
|
977
|
-
console.log(`[gonext-worker] local_health ${jobId} check mlx HTTP ${
|
|
978
|
-
|
|
1026
|
+
console.log(`[gonext-worker] local_health ${jobId} check mlx HTTP ${root} port=${port ?? "default"}`);
|
|
1027
|
+
const result = await checkOpenAiModels(root, payload?.mlxApiKey ?? "", {
|
|
979
1028
|
workerHostId: targetWorkerHostId,
|
|
980
|
-
hostName: targetWorkerHostName || sourceLabelFromBase(
|
|
1029
|
+
hostName: targetWorkerHostName || sourceLabelFromBase(root),
|
|
1030
|
+
port,
|
|
981
1031
|
});
|
|
982
1032
|
console.log(
|
|
983
|
-
`[gonext-worker] local_health ${jobId} mlx HTTP online=${
|
|
1033
|
+
`[gonext-worker] local_health ${jobId} mlx HTTP online=${result.online} models=${result.models.length} took=${((Date.now() - mlxStart) / 1000).toFixed(2)}s`
|
|
984
1034
|
);
|
|
1035
|
+
mlxHttpResults.push(result);
|
|
985
1036
|
}
|
|
1037
|
+
// Merge all per-port results
|
|
1038
|
+
const mlxModelDedup = new Map();
|
|
1039
|
+
let mlxHttpOnline = false;
|
|
1040
|
+
for (const r of mlxHttpResults) {
|
|
1041
|
+
if (r.online) mlxHttpOnline = true;
|
|
1042
|
+
for (const m of r.models) {
|
|
1043
|
+
if (!mlxModelDedup.has(m.value)) mlxModelDedup.set(m.value, m);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const mlxHttp = mlxRoots.length > 0 ? {
|
|
1047
|
+
online: mlxHttpOnline,
|
|
1048
|
+
endpoint: mlxHttpResults[0]?.endpoint,
|
|
1049
|
+
models: [...mlxModelDedup.values()],
|
|
1050
|
+
} : null;
|
|
986
1051
|
|
|
987
1052
|
const wantNativeFallback =
|
|
988
1053
|
mlxRoot &&
|
|
@@ -1001,6 +1066,11 @@ async function runLocalHealthJob(job) {
|
|
|
1001
1066
|
);
|
|
1002
1067
|
}
|
|
1003
1068
|
|
|
1069
|
+
for (const rawPath of modelPathChecks) {
|
|
1070
|
+
const check = await checkModelPathExists(rawPath);
|
|
1071
|
+
modelPathStatus.push(check);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1004
1074
|
let mlx = null;
|
|
1005
1075
|
if (mlxRoot || mlxNative?.available) {
|
|
1006
1076
|
const httpOk = Boolean(mlxHttp?.online && (mlxHttp?.models?.length ?? 0) > 0);
|
|
@@ -1055,6 +1125,12 @@ async function runLocalHealthJob(job) {
|
|
|
1055
1125
|
}
|
|
1056
1126
|
: undefined,
|
|
1057
1127
|
mlx,
|
|
1128
|
+
setup:
|
|
1129
|
+
modelPathStatus.length > 0
|
|
1130
|
+
? {
|
|
1131
|
+
modelPaths: modelPathStatus,
|
|
1132
|
+
}
|
|
1133
|
+
: undefined,
|
|
1058
1134
|
};
|
|
1059
1135
|
const totalTimeSeconds = (Date.now() - start) / 1000;
|
|
1060
1136
|
const doneRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
@@ -1133,7 +1209,9 @@ async function pollOnce() {
|
|
|
1133
1209
|
const isLocalHealthByType = job.jobType === "local_health";
|
|
1134
1210
|
const isLocalHealthByModelKey = job.modelKey === "local_health";
|
|
1135
1211
|
const isLocalHealthByPayload =
|
|
1136
|
-
Array.isArray(job.payload?.ollamaBaseUrls) ||
|
|
1212
|
+
Array.isArray(job.payload?.ollamaBaseUrls) ||
|
|
1213
|
+
!!job.payload?.mlxOpenAiBaseUrl ||
|
|
1214
|
+
Array.isArray(job.payload?.mlxOpenAiBaseUrls);
|
|
1137
1215
|
if (isLocalHealthByType || isLocalHealthByModelKey || isLocalHealthByPayload) {
|
|
1138
1216
|
const task = runLocalHealthJob(job).catch((e) => {
|
|
1139
1217
|
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.46",
|
|
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",
|