@tiens.nguyen/gonext-local-worker 1.0.30 → 1.0.31

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.
@@ -3,8 +3,9 @@
3
3
  * GoNext local worker:
4
4
  * - `gonext-local-worker set <workerKey> [--api-base URL] [--poll-ms 1500]`
5
5
  * writes ~/.gonext/worker.env
6
- * - `gonext-local-worker` starts polling loop
7
- */
6
+ * - `gonext-local-worker ws-ping-test` POST worker ws-ping (needs GONEXT_* env)
7
+ * - `gonext-local-worker simulate-chat [text]` — claim next chat job, push fake reply like the real worker (needs GONEXT_* env)
8
+ * - `gonext-local-worker` — starts polling loop (claims jobs and runs models)
8
9
  import { mkdir, readFile, writeFile } from "node:fs/promises";
9
10
  import { execFile as execFileCallback } from "node:child_process";
10
11
  import { homedir, platform } from "node:os";
@@ -30,13 +31,16 @@ Usage:
30
31
  gonext-local-worker
31
32
  gonext-local-worker set <workerKey> [--api-base <url>] [--poll-ms <ms>]
32
33
  gonext-local-worker ws-ping-test # POST /api/worker/ws-ping-test (needs GONEXT_* env)
34
+ gonext-local-worker simulate-chat [text...] # claim next chat job, fake reply (stop normal worker first)
33
35
 
34
36
  Examples:
35
37
  gonext-local-worker set abc123 --api-base https://hwohu56e8d.execute-api.ap-southeast-1.amazonaws.com
36
38
  gonext-local-worker
37
39
  gonext-local-worker ws-ping-test
40
+ gonext-local-worker simulate-chat "Hello from a fake model"
38
41
 
39
42
  Env (optional):
43
+ GONEXT_SIMULATE_TEXT default body for simulate-chat when no args
40
44
  GONEXT_MLX_LM_PYTHON Python executable for MLX LM native probe (default: python3)
41
45
  `);
42
46
  }
@@ -96,6 +100,18 @@ const apiBase = (process.env.GONEXT_API_BASE ?? "").replace(/\/+$/, "");
96
100
  const workerKey = process.env.GONEXT_WORKER_KEY ?? "";
97
101
  const pollMs = Number(process.env.GONEXT_POLL_MS ?? "1500") || 1500;
98
102
 
103
+ const CHUNK_PATH = "/api/worker/job-chunk";
104
+
105
+ async function workerFetch(path, init = {}) {
106
+ const url = `${apiBase}${path.startsWith("/") ? path : `/${path}`}`;
107
+ const headers = {
108
+ "Content-Type": "application/json",
109
+ "X-Worker-Key": workerKey,
110
+ ...(init.headers ?? {}),
111
+ };
112
+ return fetch(url, { ...init, headers });
113
+ }
114
+
99
115
  if (args[0] === "ws-ping-test") {
100
116
  if (!apiBase || !workerKey) {
101
117
  console.error(
@@ -126,6 +142,105 @@ if (args[0] === "ws-ping-test") {
126
142
  process.exit(0);
127
143
  }
128
144
 
145
+ if (args[0] === "simulate-chat") {
146
+ if (!apiBase || !workerKey) {
147
+ console.error(
148
+ "simulate-chat needs GONEXT_API_BASE and GONEXT_WORKER_KEY (run: gonext-local-worker set <key> --api-base <url>)"
149
+ );
150
+ process.exit(1);
151
+ }
152
+ const reply =
153
+ args.slice(1).join(" ").trim() ||
154
+ String(process.env.GONEXT_SIMULATE_TEXT ?? "").trim() ||
155
+ "Simulated model reply: hello from gonext-local-worker simulate-chat.";
156
+ const jobRes = await workerFetch("/api/worker/jobs/next", { method: "POST" });
157
+ if (jobRes.status === 204) {
158
+ console.error(
159
+ "[gonext-worker] simulate-chat: no pending job. In the web app, send a message that returns 202 (async local chat), then run this again. Stop the normal worker so it does not claim the job first."
160
+ );
161
+ process.exit(1);
162
+ }
163
+ if (!jobRes.ok) {
164
+ const t = await jobRes.text().catch(() => "");
165
+ console.error(`[gonext-worker] simulate-chat: jobs/next failed ${jobRes.status}: ${t}`);
166
+ process.exit(1);
167
+ }
168
+ const job = await jobRes.json();
169
+ const jobId = job?.jobId;
170
+ if (!jobId) {
171
+ console.error("[gonext-worker] simulate-chat: invalid jobs/next response (missing jobId).");
172
+ process.exit(1);
173
+ }
174
+ const isLocalHealth =
175
+ job.jobType === "local_health" || job.modelKey === "local_health";
176
+ if (isLocalHealth) {
177
+ await workerFetch(`/api/worker/jobs/${jobId}`, {
178
+ method: "PATCH",
179
+ body: JSON.stringify({
180
+ jobStatus: "failed",
181
+ errorMessage:
182
+ "simulate-chat: claimed a local_health job. Mark failed so you can retry. Queue a chat message (not Settings refresh) and stop the normal worker before simulate-chat.",
183
+ totalTimeSeconds: 0,
184
+ }),
185
+ });
186
+ console.error(
187
+ "[gonext-worker] simulate-chat: claimed local_health instead of chat. Job marked failed. Retry with only a pending chat job."
188
+ );
189
+ process.exit(1);
190
+ }
191
+ const start = Date.now();
192
+ const runRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
193
+ method: "PATCH",
194
+ body: JSON.stringify({ jobStatus: "running" }),
195
+ });
196
+ if (!runRes.ok) {
197
+ const errBody = await runRes.text().catch(() => "");
198
+ console.error(
199
+ `[gonext-worker] simulate-chat: mark running failed ${runRes.status}${errBody ? `: ${errBody}` : ""}`
200
+ );
201
+ process.exit(1);
202
+ }
203
+ const sliceSize = 160;
204
+ for (let i = 0; i < reply.length; i += sliceSize) {
205
+ const slice = reply.slice(i, i + sliceSize);
206
+ if (!slice) continue;
207
+ const chunkRes = await workerFetch(CHUNK_PATH, {
208
+ method: "POST",
209
+ body: JSON.stringify({ jobId, text: slice }),
210
+ });
211
+ if (!chunkRes.ok && chunkRes.status !== 204) {
212
+ const snippet = (await chunkRes.text().catch(() => "")).trim().slice(0, 400);
213
+ console.error(
214
+ `[gonext-worker] simulate-chat: job-chunk failed ${chunkRes.status} jobId=${jobId}` +
215
+ (snippet ? ` response=${snippet}` : "")
216
+ );
217
+ process.exit(1);
218
+ }
219
+ }
220
+ const totalTimeSeconds = (Date.now() - start) / 1000;
221
+ const doneRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
222
+ method: "PATCH",
223
+ body: JSON.stringify({
224
+ jobStatus: "completed",
225
+ resultText: reply,
226
+ tokenCount: Math.max(1, Math.ceil(reply.length / 4)),
227
+ totalTimeSeconds,
228
+ }),
229
+ });
230
+ if (!doneRes.ok) {
231
+ const snippet = (await doneRes.text().catch(() => "")).trim().slice(0, 400);
232
+ console.error(
233
+ `[gonext-worker] simulate-chat: complete PATCH failed ${doneRes.status}` +
234
+ (snippet ? `: ${snippet}` : "")
235
+ );
236
+ process.exit(1);
237
+ }
238
+ console.log(
239
+ `[gonext-worker] simulate-chat OK jobId=${jobId} (${totalTimeSeconds.toFixed(2)}s, ${reply.length} chars) — same HTTP path as the real worker (API pushes worker_job_chunk / worker_job to the web app).`
240
+ );
241
+ process.exit(0);
242
+ }
243
+
129
244
  if (!apiBase || !workerKey) {
130
245
  console.error(
131
246
  "Set GONEXT_API_BASE (HTTP API origin, no /api suffix) and GONEXT_WORKER_KEY."
@@ -133,7 +248,6 @@ if (!apiBase || !workerKey) {
133
248
  process.exit(1);
134
249
  }
135
250
 
136
- const CHUNK_PATH = "/api/worker/job-chunk";
137
251
  console.log(
138
252
  `[gonext-worker] API base ${apiBase} — streaming chunks POST ${apiBase}${CHUNK_PATH}`
139
253
  );
@@ -156,16 +270,6 @@ function toOpenAIMessages(messages) {
156
270
  });
157
271
  }
158
272
 
159
- async function workerFetch(path, init = {}) {
160
- const url = `${apiBase}${path.startsWith("/") ? path : `/${path}`}`;
161
- const headers = {
162
- "Content-Type": "application/json",
163
- "X-Worker-Key": workerKey,
164
- ...(init.headers ?? {}),
165
- };
166
- return fetch(url, { ...init, headers });
167
- }
168
-
169
273
  async function runChatJob(job) {
170
274
  const { jobId, payload } = job;
171
275
  if (!payload || !Array.isArray(payload.messages)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
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",