@tiens.nguyen/gonext-local-worker 1.0.30 → 1.0.33
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 +117 -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(
|
|
@@ -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.
|
|
3
|
+
"version": "1.0.33",
|
|
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",
|