@tiens.nguyen/gonext-local-worker 1.0.11 → 1.0.13

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.
@@ -6,9 +6,14 @@
6
6
  * - `gonext-local-worker` starts polling loop
7
7
  */
8
8
  import { mkdir, readFile, writeFile } from "node:fs/promises";
9
- import { homedir } from "node:os";
9
+ import { execFile as execFileCallback } from "node:child_process";
10
+ import { homedir, platform } from "node:os";
10
11
  import { join } from "node:path";
12
+ import { promisify } from "node:util";
11
13
  import dotenv from "dotenv";
14
+
15
+ /** Avoid `node:child_process/promises` — not available on some Node builds / older runtimes. */
16
+ const execFile = promisify(execFileCallback);
12
17
  import OpenAI from "openai";
13
18
 
14
19
  const ENV_FILE = join(homedir(), ".gonext", "worker.env");
@@ -28,6 +33,9 @@ Usage:
28
33
  Examples:
29
34
  gonext-local-worker set abc123 --api-base https://hwohu56e8d.execute-api.ap-southeast-1.amazonaws.com
30
35
  gonext-local-worker
36
+
37
+ Env (optional):
38
+ GONEXT_MLX_LM_PYTHON Python executable for MLX LM native probe (default: python3)
31
39
  `);
32
40
  }
33
41
 
@@ -228,6 +236,50 @@ async function checkOpenAiModels(base, apiKey) {
228
236
  }
229
237
  }
230
238
 
239
+ /** True MLX LM check: import mlx_lm in Python (macOS). Not the OpenAI HTTP surface. */
240
+ async function checkMlxLmNativeImport() {
241
+ const preferred = (process.env.GONEXT_MLX_LM_PYTHON ?? "").trim() || "python3";
242
+ const code = [
243
+ "import sys",
244
+ "try:",
245
+ " import mlx_lm",
246
+ " v = getattr(mlx_lm, '__version__', None)",
247
+ " print(v or 'ok')",
248
+ "except Exception:",
249
+ " sys.exit(1)",
250
+ ].join("\n");
251
+
252
+ const candidates = [preferred];
253
+ if (preferred === "python3") candidates.push("python");
254
+
255
+ const tried = [];
256
+ for (const exe of [...new Set(candidates)]) {
257
+ tried.push(exe);
258
+ try {
259
+ const { stdout } = await execFile(exe, ["-c", code], {
260
+ timeout: 15000,
261
+ maxBuffer: 65536,
262
+ windowsHide: true,
263
+ });
264
+ const version = String(stdout ?? "").trim();
265
+ return {
266
+ available: true,
267
+ python: exe,
268
+ version: version || undefined,
269
+ method: "python_import_mlx_lm",
270
+ };
271
+ } catch {
272
+ /* try next */
273
+ }
274
+ }
275
+ return {
276
+ available: false,
277
+ python: preferred,
278
+ method: "python_import_mlx_lm",
279
+ error: `Could not import mlx_lm (tried: ${tried.join(", ")})`,
280
+ };
281
+ }
282
+
231
283
  async function runLocalHealthJob(job) {
232
284
  const { jobId, payload } = job;
233
285
  const start = Date.now();
@@ -269,15 +321,66 @@ async function runLocalHealthJob(job) {
269
321
  }
270
322
  }
271
323
  const mlxRoot = normalizeOpenAiV1Root(payload?.mlxOpenAiBaseUrl);
272
- let mlx = null;
324
+ let mlxHttp = null;
325
+ let mlxNative = null;
326
+
273
327
  if (mlxRoot) {
274
328
  const mlxStart = Date.now();
275
- console.log(`[gonext-worker] local_health ${jobId} check mlx ${mlxRoot}`);
276
- mlx = await checkOpenAiModels(mlxRoot, payload?.mlxApiKey ?? "");
329
+ console.log(`[gonext-worker] local_health ${jobId} check mlx HTTP ${mlxRoot}`);
330
+ mlxHttp = await checkOpenAiModels(mlxRoot, payload?.mlxApiKey ?? "");
277
331
  console.log(
278
- `[gonext-worker] local_health ${jobId} mlx result ${mlxRoot} online=${mlx.online} models=${mlx.models.length} took=${((Date.now() - mlxStart) / 1000).toFixed(2)}s`
332
+ `[gonext-worker] local_health ${jobId} mlx HTTP online=${mlxHttp.online} models=${mlxHttp.models.length} took=${((Date.now() - mlxStart) / 1000).toFixed(2)}s`
279
333
  );
280
334
  }
335
+
336
+ const wantNativeFallback =
337
+ mlxRoot &&
338
+ payload?.mlxNativeFallback !== false &&
339
+ platform() === "darwin" &&
340
+ (!mlxHttp?.online || (mlxHttp?.models?.length ?? 0) === 0);
341
+
342
+ if (wantNativeFallback) {
343
+ const t0 = Date.now();
344
+ console.log(
345
+ `[gonext-worker] local_health ${jobId} mlx native probe (Python mlx_lm import)`
346
+ );
347
+ mlxNative = await checkMlxLmNativeImport();
348
+ console.log(
349
+ `[gonext-worker] local_health ${jobId} mlx native available=${mlxNative.available} took=${((Date.now() - t0) / 1000).toFixed(2)}s`
350
+ );
351
+ }
352
+
353
+ let mlx = null;
354
+ if (mlxRoot || mlxNative?.available) {
355
+ const httpOk = Boolean(mlxHttp?.online && (mlxHttp?.models?.length ?? 0) > 0);
356
+ const nativeOk = mlxNative?.available === true;
357
+ mlx = {
358
+ configured: httpOk || nativeOk,
359
+ online: httpOk || nativeOk,
360
+ models: httpOk
361
+ ? mlxHttp.models
362
+ : nativeOk
363
+ ? [
364
+ {
365
+ id: "mlx_lm_native",
366
+ name: mlxNative.version
367
+ ? `MLX LM (${mlxNative.version})`
368
+ : "MLX LM (Python import OK)",
369
+ value: "mlx:mlx_lm_native",
370
+ },
371
+ ]
372
+ : [],
373
+ endpoint: mlxHttp?.endpoint,
374
+ http: mlxHttp
375
+ ? {
376
+ online: mlxHttp.online,
377
+ endpoint: mlxHttp.endpoint,
378
+ modelCount: mlxHttp.models.length,
379
+ }
380
+ : undefined,
381
+ native: mlxNative ?? undefined,
382
+ };
383
+ }
281
384
  const result = {
282
385
  ollama:
283
386
  ollamaBases.length > 0
@@ -289,14 +392,7 @@ async function runLocalHealthJob(job) {
289
392
  sources: ollamaSources,
290
393
  }
291
394
  : undefined,
292
- mlx: mlx
293
- ? {
294
- configured: mlx.models.length > 0,
295
- online: mlx.online,
296
- models: mlx.models,
297
- endpoint: mlx.endpoint,
298
- }
299
- : undefined,
395
+ mlx,
300
396
  };
301
397
  const totalTimeSeconds = (Date.now() - start) / 1000;
302
398
  await workerFetch(`/api/worker/jobs/${jobId}`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
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",