@tiens.nguyen/gonext-local-worker 1.0.29 → 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(
@@ -115,12 +131,116 @@ if (args[0] === "ws-ping-test") {
115
131
  const text = await res.text();
116
132
  if (!res.ok) {
117
133
  console.error(`[gonext-worker] ws-ping-test failed ${res.status}: ${text}`);
134
+ if (res.status === 404 && text.includes("Cannot POST")) {
135
+ console.error(
136
+ "[gonext-worker] DEPLOY: this API does not have POST /api/worker/ws-ping-test yet. From repo root: sam build && sam deploy (or ship updated server-bundle to your Node host)."
137
+ );
138
+ }
118
139
  process.exit(1);
119
140
  }
120
141
  console.log("[gonext-worker] ws-ping-test OK:", text);
121
142
  process.exit(0);
122
143
  }
123
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
+
124
244
  if (!apiBase || !workerKey) {
125
245
  console.error(
126
246
  "Set GONEXT_API_BASE (HTTP API origin, no /api suffix) and GONEXT_WORKER_KEY."
@@ -128,7 +248,6 @@ if (!apiBase || !workerKey) {
128
248
  process.exit(1);
129
249
  }
130
250
 
131
- const CHUNK_PATH = "/api/worker/job-chunk";
132
251
  console.log(
133
252
  `[gonext-worker] API base ${apiBase} — streaming chunks POST ${apiBase}${CHUNK_PATH}`
134
253
  );
@@ -151,16 +270,6 @@ function toOpenAIMessages(messages) {
151
270
  });
152
271
  }
153
272
 
154
- async function workerFetch(path, init = {}) {
155
- const url = `${apiBase}${path.startsWith("/") ? path : `/${path}`}`;
156
- const headers = {
157
- "Content-Type": "application/json",
158
- "X-Worker-Key": workerKey,
159
- ...(init.headers ?? {}),
160
- };
161
- return fetch(url, { ...init, headers });
162
- }
163
-
164
273
  async function runChatJob(job) {
165
274
  const { jobId, payload } = job;
166
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.29",
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",