@tiens.nguyen/gonext-local-worker 1.0.44 → 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 +114 -13
- 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";
|
|
@@ -20,7 +20,8 @@ import OpenAI from "openai";
|
|
|
20
20
|
const execFile = promisify(execFileCallback);
|
|
21
21
|
|
|
22
22
|
const ENV_FILE = join(homedir(), ".gonext", "worker.env");
|
|
23
|
-
|
|
23
|
+
const preloadedWorkerKey = String(process.env.GONEXT_WORKER_KEY ?? "").trim();
|
|
24
|
+
const envFileLoad = dotenv.config({ path: ENV_FILE });
|
|
24
25
|
dotenv.config();
|
|
25
26
|
|
|
26
27
|
const args = process.argv.slice(2);
|
|
@@ -31,6 +32,13 @@ function workerKeyFingerprint(secret) {
|
|
|
31
32
|
return createHash("sha256").update(v, "utf8").digest("hex").slice(0, 12);
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function maskWorkerKey(secret) {
|
|
36
|
+
const v = String(secret ?? "").trim();
|
|
37
|
+
if (!v) return "(empty)";
|
|
38
|
+
if (v.length <= 12) return `${v.slice(0, 3)}***`;
|
|
39
|
+
return `${v.slice(0, 8)}...${v.slice(-4)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
function printHelp() {
|
|
35
43
|
console.log(`
|
|
36
44
|
gonext-local-worker
|
|
@@ -108,6 +116,16 @@ const apiBase = String(process.env.GONEXT_API_BASE ?? "")
|
|
|
108
116
|
.trim()
|
|
109
117
|
.replace(/\/+$/, "");
|
|
110
118
|
const workerKey = String(process.env.GONEXT_WORKER_KEY ?? "").trim();
|
|
119
|
+
const envFileWorkerKey = String(envFileLoad.parsed?.GONEXT_WORKER_KEY ?? "").trim();
|
|
120
|
+
const workerKeySource = (() => {
|
|
121
|
+
if (preloadedWorkerKey && workerKey === preloadedWorkerKey) {
|
|
122
|
+
return "process.env";
|
|
123
|
+
}
|
|
124
|
+
if (envFileWorkerKey && workerKey === envFileWorkerKey) {
|
|
125
|
+
return ENV_FILE;
|
|
126
|
+
}
|
|
127
|
+
return "unknown";
|
|
128
|
+
})();
|
|
111
129
|
const pollMs = 500;
|
|
112
130
|
const localHealthConcurrency = 4;
|
|
113
131
|
|
|
@@ -294,6 +312,11 @@ console.log(
|
|
|
294
312
|
workerKey
|
|
295
313
|
)}`
|
|
296
314
|
);
|
|
315
|
+
console.log(
|
|
316
|
+
`[gonext-worker] worker key ${maskWorkerKey(workerKey)} (keyfp=${workerKeyFingerprint(
|
|
317
|
+
workerKey
|
|
318
|
+
)}, source=${workerKeySource})`
|
|
319
|
+
);
|
|
297
320
|
|
|
298
321
|
function toOpenAIMessages(messages) {
|
|
299
322
|
return (Array.isArray(messages) ? messages : []).map((m) => {
|
|
@@ -791,6 +814,26 @@ function sourceLabelFromBase(base) {
|
|
|
791
814
|
}
|
|
792
815
|
}
|
|
793
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
|
+
|
|
794
837
|
/** Log assistant text to stdout; cap size so huge replies do not flood the terminal. */
|
|
795
838
|
function logModelResponseToWorker(jobId, modelId, text) {
|
|
796
839
|
const max = 12000;
|
|
@@ -832,12 +875,16 @@ async function checkOpenAiModels(base, apiKey, source) {
|
|
|
832
875
|
typeof source?.workerHostId === "string" ? source.workerHostId.trim() : "";
|
|
833
876
|
const hostSourceLabel =
|
|
834
877
|
typeof source?.hostName === "string" ? source.hostName.trim() : "";
|
|
878
|
+
const sourcePort = typeof source?.port === "number" ? source.port : null;
|
|
835
879
|
try {
|
|
836
880
|
const res = await fetch(endpoint, { method: "GET", headers });
|
|
837
881
|
if (!res.ok) return { online: false, endpoint, models: [] };
|
|
838
882
|
const j = await res.json();
|
|
839
|
-
const
|
|
840
|
-
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})` : "";
|
|
841
888
|
const models = (j.data ?? [])
|
|
842
889
|
.map((d) => d.id)
|
|
843
890
|
.filter(Boolean)
|
|
@@ -902,8 +949,11 @@ async function runLocalHealthJob(job) {
|
|
|
902
949
|
const ollamaPayloadCount = Array.isArray(payload?.ollamaBaseUrls)
|
|
903
950
|
? payload.ollamaBaseUrls.length
|
|
904
951
|
: 0;
|
|
952
|
+
const mlxUrlCount = Array.isArray(payload?.mlxOpenAiBaseUrls)
|
|
953
|
+
? payload.mlxOpenAiBaseUrls.length
|
|
954
|
+
: payload?.mlxOpenAiBaseUrl ? 1 : 0;
|
|
905
955
|
console.log(
|
|
906
|
-
`[gonext-worker] local_health ${jobId} start (ollamaUrls=${ollamaPayloadCount},
|
|
956
|
+
`[gonext-worker] local_health ${jobId} start (ollamaUrls=${ollamaPayloadCount}, mlxUrls=${mlxUrlCount})`
|
|
907
957
|
);
|
|
908
958
|
const runRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
909
959
|
method: "PATCH",
|
|
@@ -937,7 +987,13 @@ async function runLocalHealthJob(job) {
|
|
|
937
987
|
if (!dedup.has(m.value)) dedup.set(m.value, m);
|
|
938
988
|
}
|
|
939
989
|
}
|
|
940
|
-
|
|
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;
|
|
941
997
|
const targetWorkerHostId =
|
|
942
998
|
typeof payload?.targetWorkerHostId === "string"
|
|
943
999
|
? payload.targetWorkerHostId.trim()
|
|
@@ -946,20 +1002,52 @@ async function runLocalHealthJob(job) {
|
|
|
946
1002
|
typeof payload?.targetWorkerHostName === "string"
|
|
947
1003
|
? payload.targetWorkerHostName.trim()
|
|
948
1004
|
: "";
|
|
949
|
-
let mlxHttp = null;
|
|
950
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 = [];
|
|
951
1014
|
|
|
952
|
-
|
|
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 */ }
|
|
953
1025
|
const mlxStart = Date.now();
|
|
954
|
-
console.log(`[gonext-worker] local_health ${jobId} check mlx HTTP ${
|
|
955
|
-
|
|
1026
|
+
console.log(`[gonext-worker] local_health ${jobId} check mlx HTTP ${root} port=${port ?? "default"}`);
|
|
1027
|
+
const result = await checkOpenAiModels(root, payload?.mlxApiKey ?? "", {
|
|
956
1028
|
workerHostId: targetWorkerHostId,
|
|
957
|
-
hostName: targetWorkerHostName || sourceLabelFromBase(
|
|
1029
|
+
hostName: targetWorkerHostName || sourceLabelFromBase(root),
|
|
1030
|
+
port,
|
|
958
1031
|
});
|
|
959
1032
|
console.log(
|
|
960
|
-
`[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`
|
|
961
1034
|
);
|
|
1035
|
+
mlxHttpResults.push(result);
|
|
962
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;
|
|
963
1051
|
|
|
964
1052
|
const wantNativeFallback =
|
|
965
1053
|
mlxRoot &&
|
|
@@ -978,6 +1066,11 @@ async function runLocalHealthJob(job) {
|
|
|
978
1066
|
);
|
|
979
1067
|
}
|
|
980
1068
|
|
|
1069
|
+
for (const rawPath of modelPathChecks) {
|
|
1070
|
+
const check = await checkModelPathExists(rawPath);
|
|
1071
|
+
modelPathStatus.push(check);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
981
1074
|
let mlx = null;
|
|
982
1075
|
if (mlxRoot || mlxNative?.available) {
|
|
983
1076
|
const httpOk = Boolean(mlxHttp?.online && (mlxHttp?.models?.length ?? 0) > 0);
|
|
@@ -1032,6 +1125,12 @@ async function runLocalHealthJob(job) {
|
|
|
1032
1125
|
}
|
|
1033
1126
|
: undefined,
|
|
1034
1127
|
mlx,
|
|
1128
|
+
setup:
|
|
1129
|
+
modelPathStatus.length > 0
|
|
1130
|
+
? {
|
|
1131
|
+
modelPaths: modelPathStatus,
|
|
1132
|
+
}
|
|
1133
|
+
: undefined,
|
|
1035
1134
|
};
|
|
1036
1135
|
const totalTimeSeconds = (Date.now() - start) / 1000;
|
|
1037
1136
|
const doneRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
@@ -1110,7 +1209,9 @@ async function pollOnce() {
|
|
|
1110
1209
|
const isLocalHealthByType = job.jobType === "local_health";
|
|
1111
1210
|
const isLocalHealthByModelKey = job.modelKey === "local_health";
|
|
1112
1211
|
const isLocalHealthByPayload =
|
|
1113
|
-
Array.isArray(job.payload?.ollamaBaseUrls) ||
|
|
1212
|
+
Array.isArray(job.payload?.ollamaBaseUrls) ||
|
|
1213
|
+
!!job.payload?.mlxOpenAiBaseUrl ||
|
|
1214
|
+
Array.isArray(job.payload?.mlxOpenAiBaseUrls);
|
|
1114
1215
|
if (isLocalHealthByType || isLocalHealthByModelKey || isLocalHealthByPayload) {
|
|
1115
1216
|
const task = runLocalHealthJob(job).catch((e) => {
|
|
1116
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",
|