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

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