@elisym/cli 0.10.0 → 0.11.1
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 +560 -47
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools
|
|
|
8
8
|
import YAML from 'yaml';
|
|
9
9
|
import { Command } from 'commander';
|
|
10
10
|
import { createHash } from 'node:crypto';
|
|
11
|
+
import { LlmHealthMonitor, startLlmHeartbeat, createFreeLlmLimiterSet, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey } from '@elisym/sdk/llm-health';
|
|
11
12
|
import { lookup } from 'node:dns/promises';
|
|
12
13
|
import { Socket } from 'node:net';
|
|
13
14
|
import pino from 'pino';
|
|
@@ -149,7 +150,54 @@ async function verifyKey(apiKey, signal) {
|
|
|
149
150
|
return { ok: false, reason: "unavailable", error: message };
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
+
function bodyLooksLikeBilling(body) {
|
|
154
|
+
const lower = body.toLowerCase();
|
|
155
|
+
return BILLING_BODY_MARKERS.some((marker) => lower.includes(marker));
|
|
156
|
+
}
|
|
157
|
+
async function verifyKeyDeep(apiKey, model, signal) {
|
|
158
|
+
try {
|
|
159
|
+
const response = await fetchWithTimeout(
|
|
160
|
+
"https://api.anthropic.com/v1/messages",
|
|
161
|
+
{
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
"x-api-key": apiKey,
|
|
166
|
+
"anthropic-version": "2023-06-01"
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
model,
|
|
170
|
+
max_tokens: 1,
|
|
171
|
+
messages: [{ role: "user", content: "." }]
|
|
172
|
+
})
|
|
173
|
+
},
|
|
174
|
+
signal
|
|
175
|
+
);
|
|
176
|
+
if (response.ok) {
|
|
177
|
+
await response.body?.cancel().catch(() => void 0);
|
|
178
|
+
return { ok: true };
|
|
179
|
+
}
|
|
180
|
+
const body = (await response.text().catch(() => "")).slice(0, 500);
|
|
181
|
+
if (response.status === 401 || response.status === 403) {
|
|
182
|
+
return { ok: false, reason: "invalid", status: response.status, body };
|
|
183
|
+
}
|
|
184
|
+
if (response.status === 402) {
|
|
185
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
186
|
+
}
|
|
187
|
+
if (response.status === 400 && bodyLooksLikeBilling(body)) {
|
|
188
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: "unavailable",
|
|
193
|
+
error: `HTTP ${response.status}: ${body.slice(0, 200)}`
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
return { ok: false, reason: "unavailable", error: message };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
var DEFAULT_MODEL, DEFAULT_MAX_TOKENS, FALLBACK_MODELS, AnthropicClient, BILLING_BODY_MARKERS, ANTHROPIC_PROVIDER;
|
|
153
201
|
var init_anthropic = __esm({
|
|
154
202
|
"src/llm/providers/anthropic.ts"() {
|
|
155
203
|
init_http();
|
|
@@ -270,6 +318,7 @@ var init_anthropic = __esm({
|
|
|
270
318
|
];
|
|
271
319
|
}
|
|
272
320
|
};
|
|
321
|
+
BILLING_BODY_MARKERS = ["credit balance", "billing", "insufficient"];
|
|
273
322
|
ANTHROPIC_PROVIDER = {
|
|
274
323
|
id: "anthropic",
|
|
275
324
|
displayName: "Anthropic (Claude)",
|
|
@@ -278,6 +327,7 @@ var init_anthropic = __esm({
|
|
|
278
327
|
fallbackModels: FALLBACK_MODELS,
|
|
279
328
|
fetchModels,
|
|
280
329
|
verifyKey,
|
|
330
|
+
verifyKeyDeep,
|
|
281
331
|
createClient: (config) => new AnthropicClient({
|
|
282
332
|
apiKey: config.apiKey,
|
|
283
333
|
model: config.model ?? DEFAULT_MODEL,
|
|
@@ -288,6 +338,290 @@ var init_anthropic = __esm({
|
|
|
288
338
|
}
|
|
289
339
|
});
|
|
290
340
|
|
|
341
|
+
// src/llm/providers/openai-compatible.ts
|
|
342
|
+
function createOpenAICompatibleProvider(config) {
|
|
343
|
+
const billingMarkers = [...DEFAULT_BILLING_MARKERS, ...config.extraBillingMarkers ?? []];
|
|
344
|
+
function bodyLooksLikeBilling3(body) {
|
|
345
|
+
const lower = body.toLowerCase();
|
|
346
|
+
return billingMarkers.some((marker) => lower.includes(marker));
|
|
347
|
+
}
|
|
348
|
+
async function fetchModels4(apiKey, signal) {
|
|
349
|
+
try {
|
|
350
|
+
const response = await fetchWithTimeout(
|
|
351
|
+
`${config.baseUrl}/models`,
|
|
352
|
+
{ method: "GET", headers: { Authorization: `Bearer ${apiKey}` } },
|
|
353
|
+
signal
|
|
354
|
+
);
|
|
355
|
+
if (!response.ok) {
|
|
356
|
+
return config.fallbackModels;
|
|
357
|
+
}
|
|
358
|
+
const data = await response.json();
|
|
359
|
+
const transformed = [];
|
|
360
|
+
for (const entry of data.data ?? []) {
|
|
361
|
+
const mapped = config.mapModelId ? config.mapModelId(entry.id) : entry.id;
|
|
362
|
+
if (mapped) {
|
|
363
|
+
transformed.push(mapped);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
transformed.sort();
|
|
367
|
+
return transformed.length > 0 ? transformed : config.fallbackModels;
|
|
368
|
+
} catch {
|
|
369
|
+
return config.fallbackModels;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function verifyKey3(apiKey, signal) {
|
|
373
|
+
try {
|
|
374
|
+
const response = await fetchWithTimeout(
|
|
375
|
+
`${config.baseUrl}/models`,
|
|
376
|
+
{ method: "GET", headers: { Authorization: `Bearer ${apiKey}` } },
|
|
377
|
+
signal
|
|
378
|
+
);
|
|
379
|
+
if (response.ok) {
|
|
380
|
+
await response.body?.cancel().catch(() => void 0);
|
|
381
|
+
return { ok: true };
|
|
382
|
+
}
|
|
383
|
+
const body = (await response.text().catch(() => "")).slice(0, 500);
|
|
384
|
+
if (response.status === 401 || response.status === 403) {
|
|
385
|
+
return { ok: false, reason: "invalid", status: response.status, body };
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
ok: false,
|
|
389
|
+
reason: "unavailable",
|
|
390
|
+
error: `HTTP ${response.status}: ${body.slice(0, 200)}`
|
|
391
|
+
};
|
|
392
|
+
} catch (error) {
|
|
393
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
394
|
+
return { ok: false, reason: "unavailable", error: message };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async function verifyKeyDeep3(apiKey, model, signal) {
|
|
398
|
+
try {
|
|
399
|
+
const response = await fetchWithTimeout(
|
|
400
|
+
`${config.baseUrl}/chat/completions`,
|
|
401
|
+
{
|
|
402
|
+
method: "POST",
|
|
403
|
+
headers: {
|
|
404
|
+
"Content-Type": "application/json",
|
|
405
|
+
Authorization: `Bearer ${apiKey}`
|
|
406
|
+
},
|
|
407
|
+
body: JSON.stringify({
|
|
408
|
+
model,
|
|
409
|
+
max_tokens: 1,
|
|
410
|
+
messages: [{ role: "user", content: "." }]
|
|
411
|
+
})
|
|
412
|
+
},
|
|
413
|
+
signal
|
|
414
|
+
);
|
|
415
|
+
if (response.ok) {
|
|
416
|
+
await response.body?.cancel().catch(() => void 0);
|
|
417
|
+
return { ok: true };
|
|
418
|
+
}
|
|
419
|
+
const body = (await response.text().catch(() => "")).slice(0, 500);
|
|
420
|
+
if (response.status === 401 || response.status === 403) {
|
|
421
|
+
return { ok: false, reason: "invalid", status: response.status, body };
|
|
422
|
+
}
|
|
423
|
+
if (response.status === 402) {
|
|
424
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
425
|
+
}
|
|
426
|
+
if ((response.status === 400 || response.status === 429) && bodyLooksLikeBilling3(body)) {
|
|
427
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
ok: false,
|
|
431
|
+
reason: "unavailable",
|
|
432
|
+
error: `HTTP ${response.status}: ${body.slice(0, 200)}`
|
|
433
|
+
};
|
|
434
|
+
} catch (error) {
|
|
435
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
436
|
+
return { ok: false, reason: "unavailable", error: message };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function createClient(clientConfig) {
|
|
440
|
+
return new OpenAICompatibleClient({
|
|
441
|
+
apiKey: clientConfig.apiKey,
|
|
442
|
+
baseUrl: config.baseUrl,
|
|
443
|
+
model: clientConfig.model ?? config.defaultModel,
|
|
444
|
+
maxTokens: clientConfig.maxTokens ?? DEFAULT_MAX_TOKENS2,
|
|
445
|
+
providerLabel: config.displayName,
|
|
446
|
+
logUsage: clientConfig.logUsage
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
id: config.id,
|
|
451
|
+
displayName: config.displayName,
|
|
452
|
+
envVar: config.envVar,
|
|
453
|
+
defaultModel: config.defaultModel,
|
|
454
|
+
fallbackModels: config.fallbackModels,
|
|
455
|
+
fetchModels: fetchModels4,
|
|
456
|
+
verifyKey: verifyKey3,
|
|
457
|
+
verifyKeyDeep: verifyKeyDeep3,
|
|
458
|
+
createClient
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
var DEFAULT_MAX_TOKENS2, DEFAULT_BILLING_MARKERS, OpenAICompatibleClient;
|
|
462
|
+
var init_openai_compatible = __esm({
|
|
463
|
+
"src/llm/providers/openai-compatible.ts"() {
|
|
464
|
+
init_http();
|
|
465
|
+
DEFAULT_MAX_TOKENS2 = 4096;
|
|
466
|
+
DEFAULT_BILLING_MARKERS = ["credit balance", "billing", "insufficient_quota", "insufficient"];
|
|
467
|
+
OpenAICompatibleClient = class {
|
|
468
|
+
constructor(config) {
|
|
469
|
+
this.config = config;
|
|
470
|
+
}
|
|
471
|
+
totalIn = 0;
|
|
472
|
+
totalOut = 0;
|
|
473
|
+
logTokens(usage) {
|
|
474
|
+
if (!this.config.logUsage || !usage) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const inputTokens = usage.prompt_tokens ?? 0;
|
|
478
|
+
const outputTokens = usage.completion_tokens ?? 0;
|
|
479
|
+
this.totalIn += inputTokens;
|
|
480
|
+
this.totalOut += outputTokens;
|
|
481
|
+
console.log(
|
|
482
|
+
` [LLM] ${this.config.model} tokens: in=${inputTokens} out=${outputTokens} (total: in=${this.totalIn} out=${this.totalOut})`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
async complete(systemPrompt, userInput, signal) {
|
|
486
|
+
const response = await fetchWithRetry(
|
|
487
|
+
`${this.config.baseUrl}/chat/completions`,
|
|
488
|
+
{
|
|
489
|
+
method: "POST",
|
|
490
|
+
headers: {
|
|
491
|
+
"Content-Type": "application/json",
|
|
492
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
493
|
+
},
|
|
494
|
+
body: JSON.stringify({
|
|
495
|
+
model: this.config.model,
|
|
496
|
+
max_tokens: this.config.maxTokens,
|
|
497
|
+
messages: [
|
|
498
|
+
{ role: "system", content: systemPrompt },
|
|
499
|
+
{ role: "user", content: userInput }
|
|
500
|
+
]
|
|
501
|
+
})
|
|
502
|
+
},
|
|
503
|
+
signal
|
|
504
|
+
);
|
|
505
|
+
if (!response.ok) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`${this.config.providerLabel} API error: ${response.status} ${await response.text()}`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const data = await response.json();
|
|
511
|
+
this.logTokens(data.usage);
|
|
512
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
513
|
+
}
|
|
514
|
+
async completeWithTools(systemPrompt, messages, tools, signal) {
|
|
515
|
+
const openaiTools = tools.map((tool) => ({
|
|
516
|
+
type: "function",
|
|
517
|
+
function: {
|
|
518
|
+
name: tool.name,
|
|
519
|
+
description: tool.description,
|
|
520
|
+
parameters: {
|
|
521
|
+
type: "object",
|
|
522
|
+
properties: Object.fromEntries(
|
|
523
|
+
tool.parameters.map((param) => [
|
|
524
|
+
param.name,
|
|
525
|
+
{ type: "string", description: param.description }
|
|
526
|
+
])
|
|
527
|
+
),
|
|
528
|
+
required: tool.parameters.filter((param) => param.required).map((param) => param.name)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}));
|
|
532
|
+
const response = await fetchWithRetry(
|
|
533
|
+
`${this.config.baseUrl}/chat/completions`,
|
|
534
|
+
{
|
|
535
|
+
method: "POST",
|
|
536
|
+
headers: {
|
|
537
|
+
"Content-Type": "application/json",
|
|
538
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
539
|
+
},
|
|
540
|
+
body: JSON.stringify({
|
|
541
|
+
model: this.config.model,
|
|
542
|
+
max_tokens: this.config.maxTokens,
|
|
543
|
+
messages: [{ role: "system", content: systemPrompt }, ...messages],
|
|
544
|
+
tools: openaiTools
|
|
545
|
+
})
|
|
546
|
+
},
|
|
547
|
+
signal
|
|
548
|
+
);
|
|
549
|
+
if (!response.ok) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`${this.config.providerLabel} API error: ${response.status} ${await response.text()}`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const data = await response.json();
|
|
555
|
+
this.logTokens(data.usage);
|
|
556
|
+
const message = data.choices?.[0]?.message;
|
|
557
|
+
const toolCalls = message?.tool_calls ?? [];
|
|
558
|
+
if (toolCalls.length > 0) {
|
|
559
|
+
const calls = toolCalls.map((call) => {
|
|
560
|
+
let args;
|
|
561
|
+
try {
|
|
562
|
+
args = JSON.parse(call.function?.arguments ?? "{}");
|
|
563
|
+
} catch {
|
|
564
|
+
args = {};
|
|
565
|
+
}
|
|
566
|
+
return { id: call.id ?? "", name: call.function?.name ?? "", arguments: args };
|
|
567
|
+
});
|
|
568
|
+
return { type: "tool_use", calls, assistantMessage: message };
|
|
569
|
+
}
|
|
570
|
+
return { type: "text", text: message?.content ?? "" };
|
|
571
|
+
}
|
|
572
|
+
formatToolResultMessages(results) {
|
|
573
|
+
return results.map((result) => ({
|
|
574
|
+
role: "tool",
|
|
575
|
+
tool_call_id: result.callId,
|
|
576
|
+
content: result.content
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// src/llm/providers/deepseek.ts
|
|
584
|
+
var DEFAULT_MODEL2, FALLBACK_MODELS2, DEEPSEEK_PROVIDER;
|
|
585
|
+
var init_deepseek = __esm({
|
|
586
|
+
"src/llm/providers/deepseek.ts"() {
|
|
587
|
+
init_openai_compatible();
|
|
588
|
+
DEFAULT_MODEL2 = "deepseek-chat";
|
|
589
|
+
FALLBACK_MODELS2 = ["deepseek-chat", "deepseek-reasoner"];
|
|
590
|
+
DEEPSEEK_PROVIDER = createOpenAICompatibleProvider({
|
|
591
|
+
id: "deepseek",
|
|
592
|
+
displayName: "DeepSeek",
|
|
593
|
+
envVar: "DEEPSEEK_API_KEY",
|
|
594
|
+
baseUrl: "https://api.deepseek.com/v1",
|
|
595
|
+
defaultModel: DEFAULT_MODEL2,
|
|
596
|
+
fallbackModels: FALLBACK_MODELS2
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// src/llm/providers/google.ts
|
|
602
|
+
function mapGeminiModelId(id) {
|
|
603
|
+
const stripped = id.startsWith(MODELS_PREFIX) ? id.slice(MODELS_PREFIX.length) : id;
|
|
604
|
+
return stripped.startsWith("gemini") ? stripped : null;
|
|
605
|
+
}
|
|
606
|
+
var DEFAULT_MODEL3, FALLBACK_MODELS3, MODELS_PREFIX, GOOGLE_PROVIDER;
|
|
607
|
+
var init_google = __esm({
|
|
608
|
+
"src/llm/providers/google.ts"() {
|
|
609
|
+
init_openai_compatible();
|
|
610
|
+
DEFAULT_MODEL3 = "gemini-2.5-flash";
|
|
611
|
+
FALLBACK_MODELS3 = ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"];
|
|
612
|
+
MODELS_PREFIX = "models/";
|
|
613
|
+
GOOGLE_PROVIDER = createOpenAICompatibleProvider({
|
|
614
|
+
id: "google",
|
|
615
|
+
displayName: "Google (Gemini)",
|
|
616
|
+
envVar: "GEMINI_API_KEY",
|
|
617
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
618
|
+
defaultModel: DEFAULT_MODEL3,
|
|
619
|
+
fallbackModels: FALLBACK_MODELS3,
|
|
620
|
+
mapModelId: mapGeminiModelId
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
291
625
|
// src/llm/providers/openai.ts
|
|
292
626
|
function isOpenAIReasoningModel(model) {
|
|
293
627
|
return /^o\d/.test(model) || /^gpt-5(\b|[-.])/.test(model);
|
|
@@ -303,15 +637,15 @@ async function fetchModels2(apiKey, signal) {
|
|
|
303
637
|
signal
|
|
304
638
|
);
|
|
305
639
|
if (!response.ok) {
|
|
306
|
-
return
|
|
640
|
+
return FALLBACK_MODELS4;
|
|
307
641
|
}
|
|
308
642
|
const data = await response.json();
|
|
309
643
|
const models = (data.data ?? []).map((entry) => entry.id).filter(
|
|
310
644
|
(id) => (id.startsWith("gpt-") || id.startsWith("o1") || id.startsWith("o3") || id.startsWith("o4") || id.startsWith("chatgpt-")) && !id.includes("instruct") && !id.includes("realtime") && !id.includes("audio") && !id.includes("tts") && !id.includes("whisper")
|
|
311
645
|
).sort();
|
|
312
|
-
return models.length > 0 ? models :
|
|
646
|
+
return models.length > 0 ? models : FALLBACK_MODELS4;
|
|
313
647
|
} catch {
|
|
314
|
-
return
|
|
648
|
+
return FALLBACK_MODELS4;
|
|
315
649
|
}
|
|
316
650
|
}
|
|
317
651
|
async function verifyKey2(apiKey, signal) {
|
|
@@ -342,13 +676,63 @@ async function verifyKey2(apiKey, signal) {
|
|
|
342
676
|
return { ok: false, reason: "unavailable", error: message };
|
|
343
677
|
}
|
|
344
678
|
}
|
|
345
|
-
|
|
679
|
+
function bodyLooksLikeBilling2(body) {
|
|
680
|
+
const lower = body.toLowerCase();
|
|
681
|
+
return BILLING_BODY_MARKERS2.some((marker) => lower.includes(marker));
|
|
682
|
+
}
|
|
683
|
+
async function verifyKeyDeep2(apiKey, model, signal) {
|
|
684
|
+
try {
|
|
685
|
+
const reasoning = isOpenAIReasoningModel(model);
|
|
686
|
+
const response = await fetchWithTimeout(
|
|
687
|
+
"https://api.openai.com/v1/chat/completions",
|
|
688
|
+
{
|
|
689
|
+
method: "POST",
|
|
690
|
+
headers: {
|
|
691
|
+
"Content-Type": "application/json",
|
|
692
|
+
Authorization: `Bearer ${apiKey}`
|
|
693
|
+
},
|
|
694
|
+
body: JSON.stringify({
|
|
695
|
+
model,
|
|
696
|
+
...reasoning ? { max_completion_tokens: 1 } : { max_tokens: 1 },
|
|
697
|
+
messages: [{ role: "user", content: "." }]
|
|
698
|
+
})
|
|
699
|
+
},
|
|
700
|
+
signal
|
|
701
|
+
);
|
|
702
|
+
if (response.ok) {
|
|
703
|
+
await response.body?.cancel().catch(() => void 0);
|
|
704
|
+
return { ok: true };
|
|
705
|
+
}
|
|
706
|
+
const body = (await response.text().catch(() => "")).slice(0, 500);
|
|
707
|
+
if (response.status === 401 || response.status === 403) {
|
|
708
|
+
return { ok: false, reason: "invalid", status: response.status, body };
|
|
709
|
+
}
|
|
710
|
+
if (response.status === 402) {
|
|
711
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
712
|
+
}
|
|
713
|
+
if (response.status === 429 && bodyLooksLikeBilling2(body)) {
|
|
714
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
715
|
+
}
|
|
716
|
+
if (response.status === 400 && bodyLooksLikeBilling2(body)) {
|
|
717
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
ok: false,
|
|
721
|
+
reason: "unavailable",
|
|
722
|
+
error: `HTTP ${response.status}: ${body.slice(0, 200)}`
|
|
723
|
+
};
|
|
724
|
+
} catch (error) {
|
|
725
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
726
|
+
return { ok: false, reason: "unavailable", error: message };
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
var DEFAULT_MODEL4, DEFAULT_MAX_TOKENS3, FALLBACK_MODELS4, OpenAIClient, BILLING_BODY_MARKERS2, OPENAI_PROVIDER;
|
|
346
730
|
var init_openai = __esm({
|
|
347
731
|
"src/llm/providers/openai.ts"() {
|
|
348
732
|
init_http();
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
733
|
+
DEFAULT_MODEL4 = "gpt-4o-mini";
|
|
734
|
+
DEFAULT_MAX_TOKENS3 = 4096;
|
|
735
|
+
FALLBACK_MODELS4 = ["gpt-4o", "gpt-4o-mini", "o3-mini"];
|
|
352
736
|
OpenAIClient = class {
|
|
353
737
|
constructor(config) {
|
|
354
738
|
this.config = config;
|
|
@@ -466,18 +850,20 @@ var init_openai = __esm({
|
|
|
466
850
|
}));
|
|
467
851
|
}
|
|
468
852
|
};
|
|
853
|
+
BILLING_BODY_MARKERS2 = ["credit balance", "billing", "insufficient_quota", "insufficient"];
|
|
469
854
|
OPENAI_PROVIDER = {
|
|
470
855
|
id: "openai",
|
|
471
856
|
displayName: "OpenAI (GPT)",
|
|
472
857
|
envVar: "OPENAI_API_KEY",
|
|
473
|
-
defaultModel:
|
|
474
|
-
fallbackModels:
|
|
858
|
+
defaultModel: DEFAULT_MODEL4,
|
|
859
|
+
fallbackModels: FALLBACK_MODELS4,
|
|
475
860
|
fetchModels: fetchModels2,
|
|
476
861
|
verifyKey: verifyKey2,
|
|
862
|
+
verifyKeyDeep: verifyKeyDeep2,
|
|
477
863
|
createClient: (config) => new OpenAIClient({
|
|
478
864
|
apiKey: config.apiKey,
|
|
479
|
-
model: config.model ??
|
|
480
|
-
maxTokens: config.maxTokens ??
|
|
865
|
+
model: config.model ?? DEFAULT_MODEL4,
|
|
866
|
+
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS3,
|
|
481
867
|
logUsage: config.logUsage
|
|
482
868
|
}),
|
|
483
869
|
isReasoningModel: isOpenAIReasoningModel
|
|
@@ -485,6 +871,24 @@ var init_openai = __esm({
|
|
|
485
871
|
}
|
|
486
872
|
});
|
|
487
873
|
|
|
874
|
+
// src/llm/providers/xai.ts
|
|
875
|
+
var DEFAULT_MODEL5, FALLBACK_MODELS5, XAI_PROVIDER;
|
|
876
|
+
var init_xai = __esm({
|
|
877
|
+
"src/llm/providers/xai.ts"() {
|
|
878
|
+
init_openai_compatible();
|
|
879
|
+
DEFAULT_MODEL5 = "grok-3-mini";
|
|
880
|
+
FALLBACK_MODELS5 = ["grok-4", "grok-3", "grok-3-mini"];
|
|
881
|
+
XAI_PROVIDER = createOpenAICompatibleProvider({
|
|
882
|
+
id: "xai",
|
|
883
|
+
displayName: "xAI (Grok)",
|
|
884
|
+
envVar: "XAI_API_KEY",
|
|
885
|
+
baseUrl: "https://api.x.ai/v1",
|
|
886
|
+
defaultModel: DEFAULT_MODEL5,
|
|
887
|
+
fallbackModels: FALLBACK_MODELS5
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
|
|
488
892
|
// src/llm/registry.ts
|
|
489
893
|
function registerLlmProvider(descriptor) {
|
|
490
894
|
REGISTRY.set(descriptor.id, descriptor);
|
|
@@ -502,10 +906,16 @@ var REGISTRY;
|
|
|
502
906
|
var init_registry = __esm({
|
|
503
907
|
"src/llm/registry.ts"() {
|
|
504
908
|
init_anthropic();
|
|
909
|
+
init_deepseek();
|
|
910
|
+
init_google();
|
|
505
911
|
init_openai();
|
|
912
|
+
init_xai();
|
|
506
913
|
REGISTRY = /* @__PURE__ */ new Map();
|
|
507
914
|
registerLlmProvider(ANTHROPIC_PROVIDER);
|
|
508
915
|
registerLlmProvider(OPENAI_PROVIDER);
|
|
916
|
+
registerLlmProvider(XAI_PROVIDER);
|
|
917
|
+
registerLlmProvider(GOOGLE_PROVIDER);
|
|
918
|
+
registerLlmProvider(DEEPSEEK_PROVIDER);
|
|
509
919
|
}
|
|
510
920
|
});
|
|
511
921
|
|
|
@@ -526,7 +936,7 @@ function createLlmClient(config) {
|
|
|
526
936
|
logUsage: config.logUsage
|
|
527
937
|
});
|
|
528
938
|
}
|
|
529
|
-
async function
|
|
939
|
+
async function verifyLlmApiKeyDeep(provider, apiKey, model, signal) {
|
|
530
940
|
const descriptor = getLlmProvider(provider);
|
|
531
941
|
if (!descriptor) {
|
|
532
942
|
return {
|
|
@@ -535,7 +945,7 @@ async function verifyLlmApiKey(provider, apiKey, signal) {
|
|
|
535
945
|
error: `Unknown LLM provider "${provider}"`
|
|
536
946
|
};
|
|
537
947
|
}
|
|
538
|
-
return descriptor.
|
|
948
|
+
return descriptor.verifyKeyDeep(apiKey, model, signal);
|
|
539
949
|
}
|
|
540
950
|
var init_llm = __esm({
|
|
541
951
|
"src/llm/index.ts"() {
|
|
@@ -1408,7 +1818,7 @@ var JobLedger = class {
|
|
|
1408
1818
|
init_llm();
|
|
1409
1819
|
|
|
1410
1820
|
// src/llm/resolve.ts
|
|
1411
|
-
var
|
|
1821
|
+
var DEFAULT_MAX_TOKENS4 = 4096;
|
|
1412
1822
|
function resolveSkillLlm(input, agentDefault) {
|
|
1413
1823
|
const override = input.llmOverride;
|
|
1414
1824
|
const overridePairSet = override?.provider !== void 0 && override.model !== void 0;
|
|
@@ -1426,7 +1836,7 @@ function resolveSkillLlm(input, agentDefault) {
|
|
|
1426
1836
|
error: `Skill "${input.skillName}" at ${input.skillMdPath}: LLM model is required - declare "provider" + "model" in the SKILL.md frontmatter or set agent-level llm via 'npx @elisym/cli profile <agent>'.`
|
|
1427
1837
|
};
|
|
1428
1838
|
}
|
|
1429
|
-
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ??
|
|
1839
|
+
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS4;
|
|
1430
1840
|
return { provider, model, maxTokens };
|
|
1431
1841
|
}
|
|
1432
1842
|
|
|
@@ -1452,7 +1862,7 @@ function resolveTripleForOverride(override, agentDefault) {
|
|
|
1452
1862
|
if (!provider || !model) {
|
|
1453
1863
|
return void 0;
|
|
1454
1864
|
}
|
|
1455
|
-
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ??
|
|
1865
|
+
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS4;
|
|
1456
1866
|
return { provider, model, maxTokens };
|
|
1457
1867
|
}
|
|
1458
1868
|
|
|
@@ -1546,13 +1956,14 @@ var GLOBAL_MAX_JOBS_PER_WINDOW = 200;
|
|
|
1546
1956
|
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
1547
1957
|
var GLOBAL_LIMITER_KEY = "__global__";
|
|
1548
1958
|
var AgentRuntime = class {
|
|
1549
|
-
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}) {
|
|
1959
|
+
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor) {
|
|
1550
1960
|
this.transport = transport;
|
|
1551
1961
|
this.skills = skills;
|
|
1552
1962
|
this.skillCtx = skillCtx;
|
|
1553
1963
|
this.config = config;
|
|
1554
1964
|
this.ledger = ledger;
|
|
1555
1965
|
this.callbacks = callbacks;
|
|
1966
|
+
this.healthMonitor = healthMonitor;
|
|
1556
1967
|
this.limit = pLimit(config.maxConcurrentJobs);
|
|
1557
1968
|
this.maxQueueSize = config.maxQueueSize ?? config.maxConcurrentJobs * 10;
|
|
1558
1969
|
}
|
|
@@ -1577,6 +1988,13 @@ var AgentRuntime = class {
|
|
|
1577
1988
|
maxPerWindow: GLOBAL_MAX_JOBS_PER_WINDOW,
|
|
1578
1989
|
maxKeys: 1
|
|
1579
1990
|
});
|
|
1991
|
+
/**
|
|
1992
|
+
* Two-tier limiter applied only to free LLM skills (mode='llm', price=0).
|
|
1993
|
+
* Existing per-customer + global limiters above remain in effect for all
|
|
1994
|
+
* jobs; this set adds tighter caps to prevent unpaid spam from draining
|
|
1995
|
+
* the operator's API key.
|
|
1996
|
+
*/
|
|
1997
|
+
freeLlmLimiters = createFreeLlmLimiterSet();
|
|
1580
1998
|
/** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
|
|
1581
1999
|
async fetchProtocolConfig() {
|
|
1582
2000
|
if (this.config.network !== "devnet") {
|
|
@@ -1633,8 +2051,33 @@ var AgentRuntime = class {
|
|
|
1633
2051
|
});
|
|
1634
2052
|
return;
|
|
1635
2053
|
}
|
|
2054
|
+
const matched = this.skills.route(job.tags);
|
|
2055
|
+
const isFreeLlm = matched?.mode === "llm" && matched.priceSubunits === 0;
|
|
2056
|
+
let perCustomerLimiter;
|
|
2057
|
+
let perSkillKey;
|
|
2058
|
+
if (isFreeLlm && matched) {
|
|
2059
|
+
if (!this.freeLlmLimiters.globalLimiter.peek(FREE_LLM_GLOBAL_KEY).allowed) {
|
|
2060
|
+
this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
|
|
2061
|
+
});
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
perCustomerLimiter = this.freeLlmLimiters.getPerCustomerLimiter(
|
|
2065
|
+
matched.name,
|
|
2066
|
+
matched.rateLimit
|
|
2067
|
+
);
|
|
2068
|
+
perSkillKey = freeLlmCustomerKey(job.customerId, matched.name);
|
|
2069
|
+
if (!perCustomerLimiter.peek(perSkillKey).allowed) {
|
|
2070
|
+
this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
|
|
2071
|
+
});
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
1636
2075
|
this.customerLimiter.check(job.customerId);
|
|
1637
2076
|
this.globalLimiter.check(GLOBAL_LIMITER_KEY);
|
|
2077
|
+
if (isFreeLlm && perCustomerLimiter && perSkillKey) {
|
|
2078
|
+
this.freeLlmLimiters.globalLimiter.check(FREE_LLM_GLOBAL_KEY);
|
|
2079
|
+
perCustomerLimiter.check(perSkillKey);
|
|
2080
|
+
}
|
|
1638
2081
|
this.callbacks.onJobReceived?.(job);
|
|
1639
2082
|
this.inFlight.add(job.jobId);
|
|
1640
2083
|
this.pending++;
|
|
@@ -1662,10 +2105,12 @@ var AgentRuntime = class {
|
|
|
1662
2105
|
process.on("SIGTERM", onSignal);
|
|
1663
2106
|
});
|
|
1664
2107
|
}
|
|
1665
|
-
/** Drop expired hits from
|
|
2108
|
+
/** Drop expired hits from every sliding-window limiter. */
|
|
1666
2109
|
cleanupRateLimits() {
|
|
1667
2110
|
this.customerLimiter.prune();
|
|
1668
2111
|
this.globalLimiter.prune();
|
|
2112
|
+
this.freeLlmLimiters.globalLimiter.prune();
|
|
2113
|
+
this.freeLlmLimiters.prunePerCustomer();
|
|
1669
2114
|
}
|
|
1670
2115
|
stop() {
|
|
1671
2116
|
if (this.stopped) {
|
|
@@ -1729,6 +2174,24 @@ var AgentRuntime = class {
|
|
|
1729
2174
|
if (job.input.length > LIMITS.MAX_INPUT_LENGTH) {
|
|
1730
2175
|
throw new Error(`Input too long: ${job.input.length} chars (max ${LIMITS.MAX_INPUT_LENGTH})`);
|
|
1731
2176
|
}
|
|
2177
|
+
const matched = this.skills.route(job.tags);
|
|
2178
|
+
if (this.healthMonitor && matched && matched.mode === "llm" && matched.resolvedTriple) {
|
|
2179
|
+
try {
|
|
2180
|
+
await this.healthMonitor.assertReady(
|
|
2181
|
+
matched.resolvedTriple.provider,
|
|
2182
|
+
matched.resolvedTriple.model
|
|
2183
|
+
);
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
2186
|
+
log(`[${job.jobId.slice(0, 8)}] LLM health gate refused job: ${detail}`);
|
|
2187
|
+
await this.transport.sendFeedback(job, {
|
|
2188
|
+
type: "error",
|
|
2189
|
+
message: "Service temporarily unavailable, try again later"
|
|
2190
|
+
}).catch(() => {
|
|
2191
|
+
});
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
1732
2195
|
const jobPrice = resolveJobPrice(job.tags, this.skills);
|
|
1733
2196
|
const jobAsset = resolveJobAsset(job.tags, this.skills);
|
|
1734
2197
|
let netAmount;
|
|
@@ -2325,9 +2788,10 @@ var ScriptSkill = class {
|
|
|
2325
2788
|
|
|
2326
2789
|
// src/skill/loader.ts
|
|
2327
2790
|
function buildCliSkill(parsed, entryPath) {
|
|
2791
|
+
let skill;
|
|
2328
2792
|
switch (parsed.mode) {
|
|
2329
2793
|
case "llm":
|
|
2330
|
-
|
|
2794
|
+
skill = new ScriptSkill(
|
|
2331
2795
|
parsed.name,
|
|
2332
2796
|
parsed.description,
|
|
2333
2797
|
parsed.capabilities,
|
|
@@ -2341,6 +2805,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2341
2805
|
parsed.maxToolRounds,
|
|
2342
2806
|
parsed.llmOverride
|
|
2343
2807
|
);
|
|
2808
|
+
break;
|
|
2344
2809
|
case "static-file": {
|
|
2345
2810
|
if (parsed.outputFile === void 0) {
|
|
2346
2811
|
throw new Error(
|
|
@@ -2353,7 +2818,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2353
2818
|
`SKILL.md "${parsed.name}": "output_file" must stay inside the skill directory`
|
|
2354
2819
|
);
|
|
2355
2820
|
}
|
|
2356
|
-
|
|
2821
|
+
skill = new StaticFileSkill({
|
|
2357
2822
|
name: parsed.name,
|
|
2358
2823
|
description: parsed.description,
|
|
2359
2824
|
capabilities: parsed.capabilities,
|
|
@@ -2364,6 +2829,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2364
2829
|
imageFile: parsed.imageFile,
|
|
2365
2830
|
dir: entryPath
|
|
2366
2831
|
});
|
|
2832
|
+
break;
|
|
2367
2833
|
}
|
|
2368
2834
|
case "static-script":
|
|
2369
2835
|
case "dynamic-script": {
|
|
@@ -2377,7 +2843,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2377
2843
|
throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
|
|
2378
2844
|
}
|
|
2379
2845
|
const Ctor = parsed.mode === "static-script" ? StaticScriptSkill : DynamicScriptSkill;
|
|
2380
|
-
|
|
2846
|
+
skill = new Ctor({
|
|
2381
2847
|
name: parsed.name,
|
|
2382
2848
|
description: parsed.description,
|
|
2383
2849
|
capabilities: parsed.capabilities,
|
|
@@ -2390,8 +2856,13 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2390
2856
|
imageFile: parsed.imageFile,
|
|
2391
2857
|
dir: entryPath
|
|
2392
2858
|
});
|
|
2859
|
+
break;
|
|
2393
2860
|
}
|
|
2394
2861
|
}
|
|
2862
|
+
if (parsed.rateLimit) {
|
|
2863
|
+
skill.rateLimit = parsed.rateLimit;
|
|
2864
|
+
}
|
|
2865
|
+
return skill;
|
|
2395
2866
|
}
|
|
2396
2867
|
function loadSkillsFromDir(skillsDir) {
|
|
2397
2868
|
const skills = [];
|
|
@@ -2797,6 +3268,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2797
3268
|
const dependentSkillsByProvider = /* @__PURE__ */ new Map();
|
|
2798
3269
|
let agentDefaultCacheKey;
|
|
2799
3270
|
const llmClientCache = /* @__PURE__ */ new Map();
|
|
3271
|
+
const healthMonitor = new LlmHealthMonitor();
|
|
2800
3272
|
if (llmSkills.length > 0) {
|
|
2801
3273
|
const resolutionErrors = [];
|
|
2802
3274
|
for (const skill of llmSkills) {
|
|
@@ -2811,6 +3283,11 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2811
3283
|
}
|
|
2812
3284
|
const cacheKey = cacheKeyFor(result);
|
|
2813
3285
|
triplesByKey.set(cacheKey, result);
|
|
3286
|
+
skill.resolvedTriple = {
|
|
3287
|
+
provider: result.provider,
|
|
3288
|
+
model: result.model,
|
|
3289
|
+
maxTokens: result.maxTokens
|
|
3290
|
+
};
|
|
2814
3291
|
const list = dependentSkillsByProvider.get(result.provider) ?? [];
|
|
2815
3292
|
list.push(skill.name);
|
|
2816
3293
|
dependentSkillsByProvider.set(result.provider, list);
|
|
@@ -2854,28 +3331,47 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2854
3331
|
console.error("");
|
|
2855
3332
|
process.exit(1);
|
|
2856
3333
|
}
|
|
2857
|
-
for (const
|
|
2858
|
-
const apiKey = keyByProvider.get(provider);
|
|
3334
|
+
for (const [, triple] of triplesByKey) {
|
|
3335
|
+
const apiKey = keyByProvider.get(triple.provider);
|
|
2859
3336
|
if (!apiKey) {
|
|
2860
3337
|
continue;
|
|
2861
3338
|
}
|
|
2862
|
-
const envVar = getLlmProvider(provider)?.envVar ?? `${provider.toUpperCase()}_API_KEY`;
|
|
2863
|
-
process.stdout.write(` Verifying ${provider}
|
|
2864
|
-
const verification = await
|
|
3339
|
+
const envVar = getLlmProvider(triple.provider)?.envVar ?? `${triple.provider.toUpperCase()}_API_KEY`;
|
|
3340
|
+
process.stdout.write(` Verifying ${triple.provider} ${triple.model}... `);
|
|
3341
|
+
const verification = await verifyLlmApiKeyDeep(triple.provider, apiKey, triple.model);
|
|
3342
|
+
const descriptor = getLlmProvider(triple.provider);
|
|
3343
|
+
const verifyFn = async (signal) => descriptor ? descriptor.verifyKeyDeep(apiKey, triple.model, signal) : { ok: false, reason: "unavailable", error: "no descriptor" };
|
|
3344
|
+
healthMonitor.register({
|
|
3345
|
+
provider: triple.provider,
|
|
3346
|
+
model: triple.model,
|
|
3347
|
+
verifyFn
|
|
3348
|
+
});
|
|
3349
|
+
healthMonitor.seed(triple.provider, triple.model, verification);
|
|
2865
3350
|
if (verification.ok) {
|
|
2866
3351
|
console.log("ok");
|
|
2867
3352
|
} else if (verification.reason === "invalid") {
|
|
2868
3353
|
console.log("INVALID");
|
|
2869
|
-
console.error(` ! ${provider} rejected the API key (HTTP ${verification.status}).`);
|
|
3354
|
+
console.error(` ! ${triple.provider} rejected the API key (HTTP ${verification.status}).`);
|
|
2870
3355
|
console.error(
|
|
2871
3356
|
` Update it via \`npx @elisym/cli profile ${agentName}\` or set ${envVar} to a valid key.
|
|
3357
|
+
`
|
|
3358
|
+
);
|
|
3359
|
+
process.exit(1);
|
|
3360
|
+
} else if (verification.reason === "billing") {
|
|
3361
|
+
console.log("BILLING");
|
|
3362
|
+
const detail = verification.body ? ` ${verification.body.slice(0, 200)}` : "";
|
|
3363
|
+
console.error(
|
|
3364
|
+
` ! ${triple.provider} reports a billing/quota issue for ${triple.model}.${detail}`
|
|
3365
|
+
);
|
|
3366
|
+
console.error(
|
|
3367
|
+
` Top up the account at the provider console, or set ${envVar} to a key on a funded org.
|
|
2872
3368
|
`
|
|
2873
3369
|
);
|
|
2874
3370
|
process.exit(1);
|
|
2875
3371
|
} else {
|
|
2876
3372
|
console.log("unavailable");
|
|
2877
3373
|
console.warn(
|
|
2878
|
-
` ! Could not verify ${provider}
|
|
3374
|
+
` ! Could not verify ${triple.provider} ${triple.model} (${verification.error}). Continuing - jobs will fail if the key is invalid.
|
|
2879
3375
|
`
|
|
2880
3376
|
);
|
|
2881
3377
|
}
|
|
@@ -3093,28 +3589,45 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
3093
3589
|
log: diagLog,
|
|
3094
3590
|
logger
|
|
3095
3591
|
});
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3592
|
+
let llmHeartbeat;
|
|
3593
|
+
if (llmSkills.length > 0) {
|
|
3594
|
+
llmHeartbeat = startLlmHeartbeat({
|
|
3595
|
+
monitor: healthMonitor,
|
|
3596
|
+
log: diagLog
|
|
3597
|
+
});
|
|
3598
|
+
diagLog("LLM health monitor armed (10min TTL, 10min heartbeat).");
|
|
3599
|
+
}
|
|
3600
|
+
const runtime = new AgentRuntime(
|
|
3601
|
+
transport,
|
|
3602
|
+
registry,
|
|
3603
|
+
skillCtx,
|
|
3604
|
+
runtimeConfig,
|
|
3605
|
+
ledger,
|
|
3606
|
+
{
|
|
3607
|
+
onJobReceived: (job) => {
|
|
3608
|
+
const cap = job.tags.find((t) => t !== "elisym") ?? "unknown";
|
|
3609
|
+
process.stdout.write(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}
|
|
3100
3610
|
`);
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3611
|
+
logger.info({ event: "job_received", jobId: job.jobId, capability: cap });
|
|
3612
|
+
},
|
|
3613
|
+
onJobCompleted: (jobId) => {
|
|
3614
|
+
process.stdout.write(` [job] ${jobId.slice(0, 16)} | delivered
|
|
3105
3615
|
`);
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3616
|
+
logger.info({ event: "job_delivered", jobId });
|
|
3617
|
+
},
|
|
3618
|
+
onJobError: (jobId, error) => {
|
|
3619
|
+
process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${error}
|
|
3110
3620
|
`);
|
|
3111
|
-
|
|
3621
|
+
logger.error({ event: "job_error", jobId, error });
|
|
3622
|
+
},
|
|
3623
|
+
onLog: diagLog,
|
|
3624
|
+
onStop: () => {
|
|
3625
|
+
watchdog.stop();
|
|
3626
|
+
llmHeartbeat?.stop();
|
|
3627
|
+
}
|
|
3112
3628
|
},
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
watchdog.stop();
|
|
3116
|
-
}
|
|
3117
|
-
});
|
|
3629
|
+
healthMonitor
|
|
3630
|
+
);
|
|
3118
3631
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
3119
3632
|
await runtime.run();
|
|
3120
3633
|
}
|