@auxot/worker-cli 0.1.2 → 0.1.3
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/dist/index.js +5897 -297
- package/dist/index.js.map +7 -0
- package/package.json +4 -4
- package/dist/capabilities.js +0 -125
- package/dist/debug.js +0 -54
- package/dist/gpu-detection.js +0 -171
- package/dist/gpu-id.js +0 -48
- package/dist/llama-binary.js +0 -287
- package/dist/llama-process.js +0 -203
- package/dist/llama.js +0 -207
- package/dist/model-downloader.js +0 -145
- package/dist/model-resolver.js +0 -80
- package/dist/policy-validator.js +0 -242
- package/dist/types.js +0 -4
- package/dist/websocket.js +0 -433
package/dist/websocket.js
DELETED
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocket Connection Manager
|
|
3
|
-
*
|
|
4
|
-
* Handles connection to Auxot WebSocket server with:
|
|
5
|
-
* - Hello handshake
|
|
6
|
-
* - Heartbeat keepalive
|
|
7
|
-
* - Job message handling
|
|
8
|
-
* - Automatic reconnection with exponential backoff
|
|
9
|
-
*/
|
|
10
|
-
import WebSocket from 'ws';
|
|
11
|
-
import { logServerToClient, logClientToServer } from './debug.js';
|
|
12
|
-
import { validatePolicy } from './policy-validator.js';
|
|
13
|
-
const HEARTBEAT_INTERVAL = 5000; // 5 seconds
|
|
14
|
-
const INITIAL_RETRY_DELAY = 1000; // 1 second
|
|
15
|
-
const MAX_RETRY_DELAY = 60000; // 60 seconds
|
|
16
|
-
const RETRY_MULTIPLIER = 2; // Exponential backoff multiplier
|
|
17
|
-
export class WebSocketConnection {
|
|
18
|
-
ws = null;
|
|
19
|
-
heartbeatTimer = null;
|
|
20
|
-
reconnectTimer = null;
|
|
21
|
-
gpuKey;
|
|
22
|
-
capabilities;
|
|
23
|
-
onJobCallback = null;
|
|
24
|
-
onCancelCallback = null;
|
|
25
|
-
onPolicyCallback = null;
|
|
26
|
-
onConfigAckCallback = null;
|
|
27
|
-
wsUrl = '';
|
|
28
|
-
retryDelay = INITIAL_RETRY_DELAY;
|
|
29
|
-
isConnected = false;
|
|
30
|
-
shouldReconnect = true;
|
|
31
|
-
isReconnecting = false;
|
|
32
|
-
policy = null;
|
|
33
|
-
constructor(gpuKey, capabilities) {
|
|
34
|
-
this.gpuKey = gpuKey;
|
|
35
|
-
this.capabilities = capabilities;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Connect to WebSocket server and send hello message
|
|
39
|
-
*/
|
|
40
|
-
async connect(wsUrl) {
|
|
41
|
-
this.wsUrl = wsUrl;
|
|
42
|
-
this.shouldReconnect = true;
|
|
43
|
-
return this.attemptConnection();
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Attempt to establish WebSocket connection
|
|
47
|
-
*/
|
|
48
|
-
async attemptConnection() {
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
if (!this.wsUrl) {
|
|
51
|
-
reject(new Error('No WebSocket URL configured'));
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
// Only log if not already reconnecting (first attempt or successful reconnection)
|
|
55
|
-
if (!this.isReconnecting) {
|
|
56
|
-
console.log(`Connecting to ${this.wsUrl}...`);
|
|
57
|
-
}
|
|
58
|
-
try {
|
|
59
|
-
this.ws = new WebSocket(this.wsUrl);
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
this.scheduleReconnect();
|
|
63
|
-
reject(error);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
// Set a connection timeout
|
|
67
|
-
const connectionTimeout = setTimeout(() => {
|
|
68
|
-
if (!this.isConnected) {
|
|
69
|
-
this.ws?.close();
|
|
70
|
-
this.scheduleReconnect();
|
|
71
|
-
reject(new Error('Connection timeout'));
|
|
72
|
-
}
|
|
73
|
-
}, 10000);
|
|
74
|
-
this.ws.on('open', () => {
|
|
75
|
-
// Send hello message (server will assign GPU ID)
|
|
76
|
-
const helloMsg = {
|
|
77
|
-
type: 'hello',
|
|
78
|
-
gpu_key: this.gpuKey,
|
|
79
|
-
capabilities: this.capabilities,
|
|
80
|
-
};
|
|
81
|
-
this.send(helloMsg);
|
|
82
|
-
});
|
|
83
|
-
this.ws.on('message', async (data) => {
|
|
84
|
-
try {
|
|
85
|
-
const message = JSON.parse(data.toString());
|
|
86
|
-
// Debug log (skip heartbeat_ack to reduce noise)
|
|
87
|
-
if (message.type !== 'heartbeat_ack') {
|
|
88
|
-
logServerToClient(message);
|
|
89
|
-
}
|
|
90
|
-
if (message.type === 'hello_ack') {
|
|
91
|
-
clearTimeout(connectionTimeout);
|
|
92
|
-
if (message.success) {
|
|
93
|
-
// Store policy from server
|
|
94
|
-
if (!message.policy) {
|
|
95
|
-
const errorMsg = 'Server did not send policy in hello_ack';
|
|
96
|
-
console.error(`✗ ${errorMsg}`);
|
|
97
|
-
this.shouldReconnect = false;
|
|
98
|
-
this.ws?.close();
|
|
99
|
-
reject(new Error(errorMsg));
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
this.policy = message.policy;
|
|
103
|
-
// If policy callback is registered, defer validation (will validate via config message after spawning llama.cpp)
|
|
104
|
-
if (this.onPolicyCallback) {
|
|
105
|
-
// Spawn llama.cpp and discover capabilities (validation will happen via config message)
|
|
106
|
-
try {
|
|
107
|
-
await this.onPolicyCallback(message.policy);
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
|
-
console.error('[Policy Callback] Error:', error);
|
|
111
|
-
this.shouldReconnect = false;
|
|
112
|
-
this.ws?.close();
|
|
113
|
-
reject(error);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
// Validation deferred - will happen via config message
|
|
117
|
-
// Connection is established but validation happens later
|
|
118
|
-
console.log('✓ Successfully authenticated with server');
|
|
119
|
-
console.log(` Policy: ${message.policy.model_name} (${message.policy.quantization})`);
|
|
120
|
-
console.log(' Spawning llama.cpp process...');
|
|
121
|
-
console.log(' (Capabilities validation will happen via config message)');
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
// No policy callback - validate immediately (legacy flow)
|
|
125
|
-
const validation = await validatePolicy(this.capabilities, message.policy);
|
|
126
|
-
// Log warnings if any
|
|
127
|
-
if (validation.warnings && validation.warnings.length > 0) {
|
|
128
|
-
console.warn('⚠ Policy validation warnings:');
|
|
129
|
-
validation.warnings.forEach((warning) => {
|
|
130
|
-
console.warn(` - ${warning}`);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
if (!validation.valid) {
|
|
134
|
-
console.error('✗ Policy validation failed:');
|
|
135
|
-
console.error(' Expected:');
|
|
136
|
-
console.error(` Model: ${message.policy.model_name}`);
|
|
137
|
-
console.error(` Quantization: ${message.policy.quantization}`);
|
|
138
|
-
console.error(` Context Size: ${message.policy.context_size}`);
|
|
139
|
-
console.error(` Capabilities: ${message.policy.capabilities.join(', ')}`);
|
|
140
|
-
if (message.policy.parameters) {
|
|
141
|
-
console.error(` Parameters: ${message.policy.parameters}`);
|
|
142
|
-
}
|
|
143
|
-
if (message.policy.family) {
|
|
144
|
-
console.error(` Family: ${message.policy.family}`);
|
|
145
|
-
}
|
|
146
|
-
console.error(' Discovered:');
|
|
147
|
-
console.error(` Model: ${this.capabilities.model}`);
|
|
148
|
-
console.error(` Context Size: ${this.capabilities.ctx_size}`);
|
|
149
|
-
if (this.capabilities.parameters) {
|
|
150
|
-
console.error(` Parameters: ${this.capabilities.parameters}`);
|
|
151
|
-
}
|
|
152
|
-
console.error(' Errors:');
|
|
153
|
-
validation.errors.forEach((error) => {
|
|
154
|
-
console.error(` - ${error}`);
|
|
155
|
-
});
|
|
156
|
-
console.error('\nPlease configure your llama.cpp server to match the policy requirements.');
|
|
157
|
-
this.shouldReconnect = false;
|
|
158
|
-
this.ws?.close();
|
|
159
|
-
reject(new Error(`Policy validation failed: ${validation.errors.join('; ')}`));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
console.log('✓ Successfully authenticated with server');
|
|
163
|
-
console.log(` Policy: ${message.policy.model_name} (${message.policy.quantization})`);
|
|
164
|
-
console.log('✓ Capabilities validated against policy');
|
|
165
|
-
}
|
|
166
|
-
this.isConnected = true;
|
|
167
|
-
this.isReconnecting = false;
|
|
168
|
-
this.retryDelay = INITIAL_RETRY_DELAY; // Reset retry delay on success
|
|
169
|
-
this.startHeartbeat();
|
|
170
|
-
resolve();
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
// Check if there's a reconnect_in_seconds field (duplicate connection)
|
|
174
|
-
const errorMessage = message.error || 'Authentication failed';
|
|
175
|
-
if (message.reconnect_in_seconds || message.reconnectInSeconds) {
|
|
176
|
-
const reconnectIn = message.reconnect_in_seconds || message.reconnectInSeconds;
|
|
177
|
-
console.error(`✗ Worker UUID already connected! Are you trying to connect the same worker ID twice?`);
|
|
178
|
-
console.error(` The existing connection's presence key expires in ${reconnectIn} seconds.`);
|
|
179
|
-
console.error(` Waiting ${reconnectIn} seconds and retrying automatically...`);
|
|
180
|
-
// Close connection and schedule reconnect (do NOT reject - let retry handle it)
|
|
181
|
-
this.ws?.close();
|
|
182
|
-
// Wait for the specified time before reconnecting
|
|
183
|
-
this.retryDelay = Math.max(reconnectIn * 1000, INITIAL_RETRY_DELAY);
|
|
184
|
-
this.scheduleReconnect();
|
|
185
|
-
// Resolve instead of reject - this allows the app to continue with auto-retry
|
|
186
|
-
resolve();
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
// Authentication errors (invalid key, key not found, etc.) are fatal - stop retrying
|
|
190
|
-
console.error('✗ Authentication failed:', errorMessage);
|
|
191
|
-
console.error(' Fatal error: Stopping retry attempts. Please check your GPU key and try again.');
|
|
192
|
-
// Stop retrying for authentication errors
|
|
193
|
-
this.shouldReconnect = false;
|
|
194
|
-
this.ws?.close();
|
|
195
|
-
// Don't schedule reconnect - this is a fatal error
|
|
196
|
-
reject(new Error(errorMessage));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
else if (message.type === 'config_ack') {
|
|
201
|
-
// Config acknowledged (validation happened on server)
|
|
202
|
-
if (this.onConfigAckCallback) {
|
|
203
|
-
this.onConfigAckCallback(message.success, message.error);
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
// Fallback: log and handle errors
|
|
207
|
-
if (message.success) {
|
|
208
|
-
console.log('✓ Capabilities validated by server');
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
console.error(`✗ Config validation failed: ${message.error}`);
|
|
212
|
-
this.shouldReconnect = false;
|
|
213
|
-
this.ws?.close();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
else if (message.type === 'heartbeat_ack') {
|
|
218
|
-
// Heartbeat acknowledged (silent)
|
|
219
|
-
}
|
|
220
|
-
else if (message.type === 'cancel') {
|
|
221
|
-
// Cancel message
|
|
222
|
-
console.log(`Received cancel request for job ${message.job_id}`);
|
|
223
|
-
if (this.onCancelCallback) {
|
|
224
|
-
this.onCancelCallback(message);
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
console.warn('No cancel callback registered!');
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
else if (message.type === 'job') {
|
|
231
|
-
// Job received
|
|
232
|
-
console.log(`Received job ${message.job_id}`);
|
|
233
|
-
if (this.onJobCallback) {
|
|
234
|
-
this.onJobCallback(message).catch((err) => {
|
|
235
|
-
console.error('Error processing job:', err);
|
|
236
|
-
this.sendError(message.job_id, err.message);
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
console.warn('No job callback registered!');
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
catch (error) {
|
|
245
|
-
console.error('Error parsing message:', error);
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
this.ws.on('error', (error) => {
|
|
249
|
-
clearTimeout(connectionTimeout);
|
|
250
|
-
// Silently handle connection errors during reconnection
|
|
251
|
-
});
|
|
252
|
-
this.ws.on('close', (code, reason) => {
|
|
253
|
-
clearTimeout(connectionTimeout);
|
|
254
|
-
this.isConnected = false;
|
|
255
|
-
this.stopHeartbeat();
|
|
256
|
-
if (this.shouldReconnect) {
|
|
257
|
-
if (!this.isReconnecting) {
|
|
258
|
-
console.log('WebSocket disconnected, will continue to retry...');
|
|
259
|
-
this.isReconnecting = true;
|
|
260
|
-
}
|
|
261
|
-
this.scheduleReconnect();
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
console.log('WebSocket disconnected');
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Schedule reconnection with exponential backoff
|
|
271
|
-
*/
|
|
272
|
-
scheduleReconnect() {
|
|
273
|
-
if (this.reconnectTimer || !this.shouldReconnect) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
277
|
-
this.reconnectTimer = null;
|
|
278
|
-
try {
|
|
279
|
-
await this.attemptConnection();
|
|
280
|
-
}
|
|
281
|
-
catch (error) {
|
|
282
|
-
// Connection failed, exponential backoff will continue (silently)
|
|
283
|
-
this.retryDelay = Math.min(this.retryDelay * RETRY_MULTIPLIER, MAX_RETRY_DELAY);
|
|
284
|
-
}
|
|
285
|
-
}, this.retryDelay);
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Start sending heartbeat messages
|
|
289
|
-
*/
|
|
290
|
-
startHeartbeat() {
|
|
291
|
-
this.stopHeartbeat(); // Clear any existing timer
|
|
292
|
-
this.heartbeatTimer = setInterval(() => {
|
|
293
|
-
if (this.isConnected) {
|
|
294
|
-
const heartbeatMsg = {
|
|
295
|
-
type: 'heartbeat',
|
|
296
|
-
// Server identifies worker by WebSocket connection, not GPU ID
|
|
297
|
-
};
|
|
298
|
-
this.send(heartbeatMsg);
|
|
299
|
-
}
|
|
300
|
-
}, HEARTBEAT_INTERVAL);
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Stop heartbeat timer
|
|
304
|
-
*/
|
|
305
|
-
stopHeartbeat() {
|
|
306
|
-
if (this.heartbeatTimer) {
|
|
307
|
-
clearInterval(this.heartbeatTimer);
|
|
308
|
-
this.heartbeatTimer = null;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Send message to server
|
|
313
|
-
*/
|
|
314
|
-
send(message) {
|
|
315
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
316
|
-
const jsonString = JSON.stringify(message);
|
|
317
|
-
// Debug log (skip heartbeats to reduce noise)
|
|
318
|
-
if (message.type !== 'heartbeat') {
|
|
319
|
-
logClientToServer(message);
|
|
320
|
-
}
|
|
321
|
-
this.ws.send(jsonString);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Send token to server
|
|
326
|
-
*/
|
|
327
|
-
sendToken(jobId, token) {
|
|
328
|
-
this.send({
|
|
329
|
-
type: 'token',
|
|
330
|
-
job_id: jobId,
|
|
331
|
-
token,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Send completion to server with metadata
|
|
336
|
-
*/
|
|
337
|
-
sendComplete(jobId, fullResponse, durationMs, inputTokens, outputTokens, toolCalls) {
|
|
338
|
-
this.send({
|
|
339
|
-
type: 'complete',
|
|
340
|
-
job_id: jobId,
|
|
341
|
-
full_response: fullResponse,
|
|
342
|
-
duration_ms: durationMs,
|
|
343
|
-
input_tokens: inputTokens,
|
|
344
|
-
output_tokens: outputTokens,
|
|
345
|
-
tool_calls: toolCalls,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* Send error to server
|
|
350
|
-
*/
|
|
351
|
-
sendError(jobId, error) {
|
|
352
|
-
this.send({
|
|
353
|
-
type: 'error',
|
|
354
|
-
job_id: jobId,
|
|
355
|
-
error,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Register callback for job messages
|
|
360
|
-
*/
|
|
361
|
-
onJob(callback) {
|
|
362
|
-
this.onJobCallback = callback;
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Register callback for cancel messages
|
|
366
|
-
*/
|
|
367
|
-
onCancel(callback) {
|
|
368
|
-
this.onCancelCallback = callback;
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* Register callback for policy (called when policy is received in hello_ack)
|
|
372
|
-
* Used to spawn llama.cpp process before validation
|
|
373
|
-
*/
|
|
374
|
-
onPolicy(callback) {
|
|
375
|
-
this.onPolicyCallback = callback;
|
|
376
|
-
}
|
|
377
|
-
/**
|
|
378
|
-
* Register callback for config_ack (called when server validates our config)
|
|
379
|
-
*/
|
|
380
|
-
onConfigAck(callback) {
|
|
381
|
-
this.onConfigAckCallback = callback;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Update capabilities (called after spawning llama.cpp)
|
|
385
|
-
*/
|
|
386
|
-
updateCapabilities(capabilities) {
|
|
387
|
-
this.capabilities = capabilities;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Send config message to server (called after discovering capabilities)
|
|
391
|
-
*/
|
|
392
|
-
sendConfig(capabilities) {
|
|
393
|
-
this.send({
|
|
394
|
-
type: 'config',
|
|
395
|
-
capabilities,
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Check if connected
|
|
400
|
-
*/
|
|
401
|
-
get connected() {
|
|
402
|
-
return this.isConnected;
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Get policy received from server
|
|
406
|
-
*/
|
|
407
|
-
getPolicy() {
|
|
408
|
-
return this.policy;
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Get current capabilities
|
|
412
|
-
*/
|
|
413
|
-
getCapabilities() {
|
|
414
|
-
return this.capabilities;
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Close connection
|
|
418
|
-
*/
|
|
419
|
-
close() {
|
|
420
|
-
this.shouldReconnect = false;
|
|
421
|
-
this.stopHeartbeat();
|
|
422
|
-
if (this.reconnectTimer) {
|
|
423
|
-
clearTimeout(this.reconnectTimer);
|
|
424
|
-
this.reconnectTimer = null;
|
|
425
|
-
}
|
|
426
|
-
if (this.ws) {
|
|
427
|
-
this.ws.close();
|
|
428
|
-
this.ws = null;
|
|
429
|
-
}
|
|
430
|
-
this.isConnected = false;
|
|
431
|
-
this.policy = null; // Clear policy on disconnect
|
|
432
|
-
}
|
|
433
|
-
}
|