@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.
- package/gonext-local-worker.mjs +122 -13
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -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`
|
|
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.
|
|
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",
|