@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.
@@ -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 modelSuffix = hostSourceId ? `@@host:${hostSourceId}` : "";
863
- 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})` : "";
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}, mlx=${payload?.mlxOpenAiBaseUrl ? "yes" : "no"})`
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
- 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;
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
- 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 */ }
976
1025
  const mlxStart = Date.now();
977
- console.log(`[gonext-worker] local_health ${jobId} check mlx HTTP ${mlxRoot}`);
978
- 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 ?? "", {
979
1028
  workerHostId: targetWorkerHostId,
980
- hostName: targetWorkerHostName || sourceLabelFromBase(mlxRoot),
1029
+ hostName: targetWorkerHostName || sourceLabelFromBase(root),
1030
+ port,
981
1031
  });
982
1032
  console.log(
983
- `[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`
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) || !!job.payload?.mlxOpenAiBaseUrl;
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.45",
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",