@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.
@@ -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
- dotenv.config({ path: ENV_FILE });
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 modelSuffix = hostSourceId ? `@@host:${hostSourceId}` : "";
840
- const displaySuffix = hostSourceLabel ? ` (${hostSourceLabel})` : "";
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}, mlx=${payload?.mlxOpenAiBaseUrl ? "yes" : "no"})`
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
- const mlxRoot = normalizeOpenAiV1Root(payload?.mlxOpenAiBaseUrl);
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
- if (mlxRoot) {
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 ${mlxRoot}`);
955
- mlxHttp = await checkOpenAiModels(mlxRoot, payload?.mlxApiKey ?? "", {
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(mlxRoot),
1029
+ hostName: targetWorkerHostName || sourceLabelFromBase(root),
1030
+ port,
958
1031
  });
959
1032
  console.log(
960
- `[gonext-worker] local_health ${jobId} mlx HTTP online=${mlxHttp.online} models=${mlxHttp.models.length} took=${((Date.now() - mlxStart) / 1000).toFixed(2)}s`
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) || !!job.payload?.mlxOpenAiBaseUrl;
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.44",
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",