@elisym/cli 0.9.1 → 0.11.0
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 +887 -466
- 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';
|
|
@@ -25,42 +26,643 @@ var __export = (target, all) => {
|
|
|
25
26
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
// src/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
// src/llm/providers/http.ts
|
|
30
|
+
function createAbortError() {
|
|
31
|
+
const err = new Error("The operation was aborted");
|
|
32
|
+
err.name = "AbortError";
|
|
33
|
+
return err;
|
|
34
|
+
}
|
|
35
|
+
function sleepWithSignal(ms, signal) {
|
|
36
|
+
if (signal?.aborted) {
|
|
37
|
+
return Promise.reject(createAbortError());
|
|
38
|
+
}
|
|
39
|
+
if (!signal) {
|
|
40
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
41
|
+
}
|
|
42
|
+
return new Promise((resolve3, reject) => {
|
|
43
|
+
const cleanup = () => {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
signal.removeEventListener("abort", onAbort);
|
|
46
|
+
};
|
|
47
|
+
const onAbort = () => {
|
|
48
|
+
cleanup();
|
|
49
|
+
reject(createAbortError());
|
|
50
|
+
};
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
cleanup();
|
|
53
|
+
resolve3();
|
|
54
|
+
}, ms);
|
|
55
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function fetchWithTimeout(url, init, signal) {
|
|
59
|
+
if (signal?.aborted) {
|
|
60
|
+
throw createAbortError();
|
|
61
|
+
}
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
|
|
64
|
+
const onAbort = () => controller.abort();
|
|
65
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
66
|
+
try {
|
|
67
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
68
|
+
} finally {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
signal?.removeEventListener("abort", onAbort);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function fetchWithRetry(url, init, signal) {
|
|
74
|
+
for (let attempt = 0; ; attempt++) {
|
|
75
|
+
let response;
|
|
76
|
+
try {
|
|
77
|
+
response = await fetchWithTimeout(url, init, signal);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const name = error instanceof Error ? error.name : "";
|
|
80
|
+
if (attempt >= MAX_RETRIES || name === "AbortError") {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
await sleepWithSignal(Math.min(1e3 * 2 ** attempt, 8e3), signal);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (response.ok || attempt >= MAX_RETRIES || !RETRYABLE_STATUSES.has(response.status)) {
|
|
87
|
+
return response;
|
|
88
|
+
}
|
|
89
|
+
const retryAfter = response.headers.get("retry-after");
|
|
90
|
+
const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3 || 1e3 * 2 ** attempt, 3e4) : Math.min(1e3 * 2 ** attempt, 8e3);
|
|
91
|
+
await response.body?.cancel().catch(() => void 0);
|
|
92
|
+
await sleepWithSignal(delay, signal);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
var LLM_TIMEOUT_MS, MAX_RETRIES, RETRYABLE_STATUSES;
|
|
96
|
+
var init_http = __esm({
|
|
97
|
+
"src/llm/providers/http.ts"() {
|
|
98
|
+
LLM_TIMEOUT_MS = 12e4;
|
|
99
|
+
MAX_RETRIES = 2;
|
|
100
|
+
RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
101
|
+
}
|
|
33
102
|
});
|
|
34
|
-
|
|
103
|
+
|
|
104
|
+
// src/llm/providers/anthropic.ts
|
|
105
|
+
async function fetchModels(apiKey, signal) {
|
|
35
106
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
107
|
+
const response = await fetchWithTimeout(
|
|
108
|
+
"https://api.anthropic.com/v1/models?limit=1000",
|
|
109
|
+
{
|
|
110
|
+
method: "GET",
|
|
38
111
|
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const models = (data.data ?? []).map((m) => m.id).sort();
|
|
45
|
-
return models.length > 0 ? models : FALLBACK_MODELS.anthropic;
|
|
112
|
+
},
|
|
113
|
+
signal
|
|
114
|
+
);
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
return FALLBACK_MODELS;
|
|
46
117
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
118
|
+
const data = await response.json();
|
|
119
|
+
const models = (data.data ?? []).map((entry) => entry.id).sort();
|
|
120
|
+
return models.length > 0 ? models : FALLBACK_MODELS;
|
|
121
|
+
} catch {
|
|
122
|
+
return FALLBACK_MODELS;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function verifyKey(apiKey, signal) {
|
|
126
|
+
try {
|
|
127
|
+
const response = await fetchWithTimeout(
|
|
128
|
+
"https://api.anthropic.com/v1/models?limit=1",
|
|
129
|
+
{
|
|
130
|
+
method: "GET",
|
|
131
|
+
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
|
|
132
|
+
},
|
|
133
|
+
signal
|
|
134
|
+
);
|
|
135
|
+
if (response.ok) {
|
|
136
|
+
await response.body?.cancel().catch(() => void 0);
|
|
137
|
+
return { ok: true };
|
|
138
|
+
}
|
|
139
|
+
const body = (await response.text().catch(() => "")).slice(0, 500);
|
|
140
|
+
if (response.status === 401 || response.status === 403) {
|
|
141
|
+
return { ok: false, reason: "invalid", status: response.status, body };
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
reason: "unavailable",
|
|
146
|
+
error: `HTTP ${response.status}: ${body.slice(0, 200)}`
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
150
|
+
return { ok: false, reason: "unavailable", error: message };
|
|
151
|
+
}
|
|
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;
|
|
201
|
+
var init_anthropic = __esm({
|
|
202
|
+
"src/llm/providers/anthropic.ts"() {
|
|
203
|
+
init_http();
|
|
204
|
+
DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
205
|
+
DEFAULT_MAX_TOKENS = 4096;
|
|
206
|
+
FALLBACK_MODELS = ["claude-sonnet-4-6", "claude-haiku-4-5-20251001", "claude-opus-4-6"];
|
|
207
|
+
AnthropicClient = class {
|
|
208
|
+
constructor(config) {
|
|
209
|
+
this.config = config;
|
|
210
|
+
}
|
|
211
|
+
totalIn = 0;
|
|
212
|
+
totalOut = 0;
|
|
213
|
+
logTokens(usage) {
|
|
214
|
+
if (!this.config.logUsage || !usage) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const inputTokens = usage.input_tokens ?? 0;
|
|
218
|
+
const outputTokens = usage.output_tokens ?? 0;
|
|
219
|
+
this.totalIn += inputTokens;
|
|
220
|
+
this.totalOut += outputTokens;
|
|
221
|
+
console.log(
|
|
222
|
+
` [LLM] ${this.config.model} tokens: in=${inputTokens} out=${outputTokens} (total: in=${this.totalIn} out=${this.totalOut})`
|
|
223
|
+
);
|
|
53
224
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
225
|
+
async complete(systemPrompt, userInput, signal) {
|
|
226
|
+
const response = await fetchWithRetry(
|
|
227
|
+
"https://api.anthropic.com/v1/messages",
|
|
228
|
+
{
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: {
|
|
231
|
+
"Content-Type": "application/json",
|
|
232
|
+
"x-api-key": this.config.apiKey,
|
|
233
|
+
"anthropic-version": "2023-06-01"
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify({
|
|
236
|
+
model: this.config.model,
|
|
237
|
+
max_tokens: this.config.maxTokens,
|
|
238
|
+
system: systemPrompt,
|
|
239
|
+
messages: [{ role: "user", content: userInput }]
|
|
240
|
+
})
|
|
241
|
+
},
|
|
242
|
+
signal
|
|
243
|
+
);
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
|
|
246
|
+
}
|
|
247
|
+
const data = await response.json();
|
|
248
|
+
this.logTokens(data.usage);
|
|
249
|
+
const textBlock = data.content?.find((block) => block.type === "text");
|
|
250
|
+
return textBlock?.text ?? "";
|
|
251
|
+
}
|
|
252
|
+
async completeWithTools(systemPrompt, messages, tools, signal) {
|
|
253
|
+
const anthropicTools = tools.map((tool) => ({
|
|
254
|
+
name: tool.name,
|
|
255
|
+
description: tool.description,
|
|
256
|
+
input_schema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: Object.fromEntries(
|
|
259
|
+
tool.parameters.map((param) => [
|
|
260
|
+
param.name,
|
|
261
|
+
{ type: "string", description: param.description }
|
|
262
|
+
])
|
|
263
|
+
),
|
|
264
|
+
required: tool.parameters.filter((param) => param.required).map((param) => param.name)
|
|
265
|
+
}
|
|
266
|
+
}));
|
|
267
|
+
const response = await fetchWithRetry(
|
|
268
|
+
"https://api.anthropic.com/v1/messages",
|
|
269
|
+
{
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: {
|
|
272
|
+
"Content-Type": "application/json",
|
|
273
|
+
"x-api-key": this.config.apiKey,
|
|
274
|
+
"anthropic-version": "2023-06-01"
|
|
275
|
+
},
|
|
276
|
+
body: JSON.stringify({
|
|
277
|
+
model: this.config.model,
|
|
278
|
+
max_tokens: this.config.maxTokens,
|
|
279
|
+
system: systemPrompt,
|
|
280
|
+
messages,
|
|
281
|
+
tools: anthropicTools
|
|
282
|
+
})
|
|
283
|
+
},
|
|
284
|
+
signal
|
|
285
|
+
);
|
|
286
|
+
if (!response.ok) {
|
|
287
|
+
throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
|
|
288
|
+
}
|
|
289
|
+
const data = await response.json();
|
|
290
|
+
this.logTokens(data.usage);
|
|
291
|
+
const content = data.content ?? [];
|
|
292
|
+
const toolUses = content.filter((block) => block.type === "tool_use");
|
|
293
|
+
if (toolUses.length > 0) {
|
|
294
|
+
const calls = toolUses.map((block) => ({
|
|
295
|
+
id: block.id ?? "",
|
|
296
|
+
name: block.name ?? "",
|
|
297
|
+
arguments: block.input ?? {}
|
|
298
|
+
}));
|
|
299
|
+
return {
|
|
300
|
+
type: "tool_use",
|
|
301
|
+
calls,
|
|
302
|
+
assistantMessage: { role: "assistant", content }
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const textBlock = content.find((block) => block.type === "text");
|
|
306
|
+
return { type: "text", text: textBlock?.text ?? "" };
|
|
307
|
+
}
|
|
308
|
+
formatToolResultMessages(results) {
|
|
309
|
+
return [
|
|
310
|
+
{
|
|
311
|
+
role: "user",
|
|
312
|
+
content: results.map((result) => ({
|
|
313
|
+
type: "tool_result",
|
|
314
|
+
tool_use_id: result.callId,
|
|
315
|
+
content: result.content
|
|
316
|
+
}))
|
|
317
|
+
}
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
BILLING_BODY_MARKERS = ["credit balance", "billing", "insufficient"];
|
|
322
|
+
ANTHROPIC_PROVIDER = {
|
|
323
|
+
id: "anthropic",
|
|
324
|
+
displayName: "Anthropic (Claude)",
|
|
325
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
326
|
+
defaultModel: DEFAULT_MODEL,
|
|
327
|
+
fallbackModels: FALLBACK_MODELS,
|
|
328
|
+
fetchModels,
|
|
329
|
+
verifyKey,
|
|
330
|
+
verifyKeyDeep,
|
|
331
|
+
createClient: (config) => new AnthropicClient({
|
|
332
|
+
apiKey: config.apiKey,
|
|
333
|
+
model: config.model ?? DEFAULT_MODEL,
|
|
334
|
+
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
335
|
+
logUsage: config.logUsage
|
|
336
|
+
})
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// src/llm/providers/openai.ts
|
|
342
|
+
function isOpenAIReasoningModel(model) {
|
|
343
|
+
return /^o\d/.test(model) || /^gpt-5(\b|[-.])/.test(model);
|
|
344
|
+
}
|
|
345
|
+
async function fetchModels2(apiKey, signal) {
|
|
346
|
+
try {
|
|
347
|
+
const response = await fetchWithTimeout(
|
|
348
|
+
"https://api.openai.com/v1/models",
|
|
349
|
+
{
|
|
350
|
+
method: "GET",
|
|
351
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
352
|
+
},
|
|
353
|
+
signal
|
|
354
|
+
);
|
|
355
|
+
if (!response.ok) {
|
|
356
|
+
return FALLBACK_MODELS2;
|
|
357
|
+
}
|
|
358
|
+
const data = await response.json();
|
|
359
|
+
const models = (data.data ?? []).map((entry) => entry.id).filter(
|
|
360
|
+
(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")
|
|
361
|
+
).sort();
|
|
362
|
+
return models.length > 0 ? models : FALLBACK_MODELS2;
|
|
363
|
+
} catch {
|
|
364
|
+
return FALLBACK_MODELS2;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function verifyKey2(apiKey, signal) {
|
|
368
|
+
try {
|
|
369
|
+
const response = await fetchWithTimeout(
|
|
370
|
+
"https://api.openai.com/v1/models",
|
|
371
|
+
{
|
|
372
|
+
method: "GET",
|
|
373
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
374
|
+
},
|
|
375
|
+
signal
|
|
376
|
+
);
|
|
377
|
+
if (response.ok) {
|
|
378
|
+
await response.body?.cancel().catch(() => void 0);
|
|
379
|
+
return { ok: true };
|
|
380
|
+
}
|
|
381
|
+
const body = (await response.text().catch(() => "")).slice(0, 500);
|
|
382
|
+
if (response.status === 401 || response.status === 403) {
|
|
383
|
+
return { ok: false, reason: "invalid", status: response.status, body };
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
reason: "unavailable",
|
|
388
|
+
error: `HTTP ${response.status}: ${body.slice(0, 200)}`
|
|
389
|
+
};
|
|
390
|
+
} catch (error) {
|
|
391
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
392
|
+
return { ok: false, reason: "unavailable", error: message };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function bodyLooksLikeBilling2(body) {
|
|
396
|
+
const lower = body.toLowerCase();
|
|
397
|
+
return BILLING_BODY_MARKERS2.some((marker) => lower.includes(marker));
|
|
398
|
+
}
|
|
399
|
+
async function verifyKeyDeep2(apiKey, model, signal) {
|
|
400
|
+
try {
|
|
401
|
+
const reasoning = isOpenAIReasoningModel(model);
|
|
402
|
+
const response = await fetchWithTimeout(
|
|
403
|
+
"https://api.openai.com/v1/chat/completions",
|
|
404
|
+
{
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: {
|
|
407
|
+
"Content-Type": "application/json",
|
|
408
|
+
Authorization: `Bearer ${apiKey}`
|
|
409
|
+
},
|
|
410
|
+
body: JSON.stringify({
|
|
411
|
+
model,
|
|
412
|
+
...reasoning ? { max_completion_tokens: 1 } : { max_tokens: 1 },
|
|
413
|
+
messages: [{ role: "user", content: "." }]
|
|
414
|
+
})
|
|
415
|
+
},
|
|
416
|
+
signal
|
|
417
|
+
);
|
|
418
|
+
if (response.ok) {
|
|
419
|
+
await response.body?.cancel().catch(() => void 0);
|
|
420
|
+
return { ok: true };
|
|
421
|
+
}
|
|
422
|
+
const body = (await response.text().catch(() => "")).slice(0, 500);
|
|
423
|
+
if (response.status === 401 || response.status === 403) {
|
|
424
|
+
return { ok: false, reason: "invalid", status: response.status, body };
|
|
425
|
+
}
|
|
426
|
+
if (response.status === 402) {
|
|
427
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
428
|
+
}
|
|
429
|
+
if (response.status === 429 && bodyLooksLikeBilling2(body)) {
|
|
430
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
431
|
+
}
|
|
432
|
+
if (response.status === 400 && bodyLooksLikeBilling2(body)) {
|
|
433
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
59
434
|
}
|
|
60
|
-
return
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
reason: "unavailable",
|
|
438
|
+
error: `HTTP ${response.status}: ${body.slice(0, 200)}`
|
|
439
|
+
};
|
|
440
|
+
} catch (error) {
|
|
441
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
442
|
+
return { ok: false, reason: "unavailable", error: message };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
var DEFAULT_MODEL2, DEFAULT_MAX_TOKENS2, FALLBACK_MODELS2, OpenAIClient, BILLING_BODY_MARKERS2, OPENAI_PROVIDER;
|
|
446
|
+
var init_openai = __esm({
|
|
447
|
+
"src/llm/providers/openai.ts"() {
|
|
448
|
+
init_http();
|
|
449
|
+
DEFAULT_MODEL2 = "gpt-4o-mini";
|
|
450
|
+
DEFAULT_MAX_TOKENS2 = 4096;
|
|
451
|
+
FALLBACK_MODELS2 = ["gpt-4o", "gpt-4o-mini", "o3-mini"];
|
|
452
|
+
OpenAIClient = class {
|
|
453
|
+
constructor(config) {
|
|
454
|
+
this.config = config;
|
|
455
|
+
}
|
|
456
|
+
totalIn = 0;
|
|
457
|
+
totalOut = 0;
|
|
458
|
+
isReasoningModel() {
|
|
459
|
+
return isOpenAIReasoningModel(this.config.model);
|
|
460
|
+
}
|
|
461
|
+
logTokens(usage) {
|
|
462
|
+
if (!this.config.logUsage || !usage) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const inputTokens = usage.prompt_tokens ?? 0;
|
|
466
|
+
const outputTokens = usage.completion_tokens ?? 0;
|
|
467
|
+
this.totalIn += inputTokens;
|
|
468
|
+
this.totalOut += outputTokens;
|
|
469
|
+
console.log(
|
|
470
|
+
` [LLM] ${this.config.model} tokens: in=${inputTokens} out=${outputTokens} (total: in=${this.totalIn} out=${this.totalOut})`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
async complete(systemPrompt, userInput, signal) {
|
|
474
|
+
const reasoning = this.isReasoningModel();
|
|
475
|
+
const response = await fetchWithRetry(
|
|
476
|
+
"https://api.openai.com/v1/chat/completions",
|
|
477
|
+
{
|
|
478
|
+
method: "POST",
|
|
479
|
+
headers: {
|
|
480
|
+
"Content-Type": "application/json",
|
|
481
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
482
|
+
},
|
|
483
|
+
body: JSON.stringify({
|
|
484
|
+
model: this.config.model,
|
|
485
|
+
...reasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
|
|
486
|
+
messages: [
|
|
487
|
+
{ role: reasoning ? "developer" : "system", content: systemPrompt },
|
|
488
|
+
{ role: "user", content: userInput }
|
|
489
|
+
]
|
|
490
|
+
})
|
|
491
|
+
},
|
|
492
|
+
signal
|
|
493
|
+
);
|
|
494
|
+
if (!response.ok) {
|
|
495
|
+
throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
|
|
496
|
+
}
|
|
497
|
+
const data = await response.json();
|
|
498
|
+
this.logTokens(data.usage);
|
|
499
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
500
|
+
}
|
|
501
|
+
async completeWithTools(systemPrompt, messages, tools, signal) {
|
|
502
|
+
const openaiTools = tools.map((tool) => ({
|
|
503
|
+
type: "function",
|
|
504
|
+
function: {
|
|
505
|
+
name: tool.name,
|
|
506
|
+
description: tool.description,
|
|
507
|
+
parameters: {
|
|
508
|
+
type: "object",
|
|
509
|
+
properties: Object.fromEntries(
|
|
510
|
+
tool.parameters.map((param) => [
|
|
511
|
+
param.name,
|
|
512
|
+
{ type: "string", description: param.description }
|
|
513
|
+
])
|
|
514
|
+
),
|
|
515
|
+
required: tool.parameters.filter((param) => param.required).map((param) => param.name)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}));
|
|
519
|
+
const reasoning = this.isReasoningModel();
|
|
520
|
+
const response = await fetchWithRetry(
|
|
521
|
+
"https://api.openai.com/v1/chat/completions",
|
|
522
|
+
{
|
|
523
|
+
method: "POST",
|
|
524
|
+
headers: {
|
|
525
|
+
"Content-Type": "application/json",
|
|
526
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
527
|
+
},
|
|
528
|
+
body: JSON.stringify({
|
|
529
|
+
model: this.config.model,
|
|
530
|
+
...reasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
|
|
531
|
+
messages: [
|
|
532
|
+
{ role: reasoning ? "developer" : "system", content: systemPrompt },
|
|
533
|
+
...messages
|
|
534
|
+
],
|
|
535
|
+
tools: openaiTools
|
|
536
|
+
})
|
|
537
|
+
},
|
|
538
|
+
signal
|
|
539
|
+
);
|
|
540
|
+
if (!response.ok) {
|
|
541
|
+
throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
|
|
542
|
+
}
|
|
543
|
+
const data = await response.json();
|
|
544
|
+
this.logTokens(data.usage);
|
|
545
|
+
const message = data.choices?.[0]?.message;
|
|
546
|
+
const toolCalls = message?.tool_calls ?? [];
|
|
547
|
+
if (toolCalls.length > 0) {
|
|
548
|
+
const calls = toolCalls.map((call) => {
|
|
549
|
+
let args;
|
|
550
|
+
try {
|
|
551
|
+
args = JSON.parse(call.function?.arguments ?? "{}");
|
|
552
|
+
} catch {
|
|
553
|
+
args = {};
|
|
554
|
+
}
|
|
555
|
+
return { id: call.id ?? "", name: call.function?.name ?? "", arguments: args };
|
|
556
|
+
});
|
|
557
|
+
return { type: "tool_use", calls, assistantMessage: message };
|
|
558
|
+
}
|
|
559
|
+
return { type: "text", text: message?.content ?? "" };
|
|
560
|
+
}
|
|
561
|
+
formatToolResultMessages(results) {
|
|
562
|
+
return results.map((result) => ({
|
|
563
|
+
role: "tool",
|
|
564
|
+
tool_call_id: result.callId,
|
|
565
|
+
content: result.content
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
BILLING_BODY_MARKERS2 = ["credit balance", "billing", "insufficient_quota", "insufficient"];
|
|
570
|
+
OPENAI_PROVIDER = {
|
|
571
|
+
id: "openai",
|
|
572
|
+
displayName: "OpenAI (GPT)",
|
|
573
|
+
envVar: "OPENAI_API_KEY",
|
|
574
|
+
defaultModel: DEFAULT_MODEL2,
|
|
575
|
+
fallbackModels: FALLBACK_MODELS2,
|
|
576
|
+
fetchModels: fetchModels2,
|
|
577
|
+
verifyKey: verifyKey2,
|
|
578
|
+
verifyKeyDeep: verifyKeyDeep2,
|
|
579
|
+
createClient: (config) => new OpenAIClient({
|
|
580
|
+
apiKey: config.apiKey,
|
|
581
|
+
model: config.model ?? DEFAULT_MODEL2,
|
|
582
|
+
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS2,
|
|
583
|
+
logUsage: config.logUsage
|
|
584
|
+
}),
|
|
585
|
+
isReasoningModel: isOpenAIReasoningModel
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// src/llm/registry.ts
|
|
591
|
+
function registerLlmProvider(descriptor) {
|
|
592
|
+
REGISTRY.set(descriptor.id, descriptor);
|
|
593
|
+
}
|
|
594
|
+
function getLlmProvider(id) {
|
|
595
|
+
return REGISTRY.get(id);
|
|
596
|
+
}
|
|
597
|
+
function listLlmProviders() {
|
|
598
|
+
return Array.from(REGISTRY.values());
|
|
599
|
+
}
|
|
600
|
+
function getRegisteredProviderIds() {
|
|
601
|
+
return Array.from(REGISTRY.keys());
|
|
602
|
+
}
|
|
603
|
+
var REGISTRY;
|
|
604
|
+
var init_registry = __esm({
|
|
605
|
+
"src/llm/registry.ts"() {
|
|
606
|
+
init_anthropic();
|
|
607
|
+
init_openai();
|
|
608
|
+
REGISTRY = /* @__PURE__ */ new Map();
|
|
609
|
+
registerLlmProvider(ANTHROPIC_PROVIDER);
|
|
610
|
+
registerLlmProvider(OPENAI_PROVIDER);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// src/llm/index.ts
|
|
615
|
+
function createLlmClient(config) {
|
|
616
|
+
const descriptor = getLlmProvider(config.provider);
|
|
617
|
+
if (!descriptor) {
|
|
618
|
+
const known = getRegisteredProviderIds().join(", ") || "<none>";
|
|
619
|
+
throw new Error(`Unknown LLM provider "${config.provider}". Registered: ${known}.`);
|
|
620
|
+
}
|
|
621
|
+
if (!config.apiKey) {
|
|
622
|
+
throw new Error(`${descriptor.envVar} is required for skill runtime`);
|
|
623
|
+
}
|
|
624
|
+
return descriptor.createClient({
|
|
625
|
+
apiKey: config.apiKey,
|
|
626
|
+
model: config.model,
|
|
627
|
+
maxTokens: config.maxTokens,
|
|
628
|
+
logUsage: config.logUsage
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
async function verifyLlmApiKeyDeep(provider, apiKey, model, signal) {
|
|
632
|
+
const descriptor = getLlmProvider(provider);
|
|
633
|
+
if (!descriptor) {
|
|
634
|
+
return {
|
|
635
|
+
ok: false,
|
|
636
|
+
reason: "unavailable",
|
|
637
|
+
error: `Unknown LLM provider "${provider}"`
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
return descriptor.verifyKeyDeep(apiKey, model, signal);
|
|
641
|
+
}
|
|
642
|
+
var init_llm = __esm({
|
|
643
|
+
"src/llm/index.ts"() {
|
|
644
|
+
init_registry();
|
|
645
|
+
init_registry();
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// src/commands/init.ts
|
|
650
|
+
var init_exports = {};
|
|
651
|
+
__export(init_exports, {
|
|
652
|
+
cmdInit: () => cmdInit,
|
|
653
|
+
fetchModels: () => fetchModels3
|
|
654
|
+
});
|
|
655
|
+
async function fetchModels3(provider, apiKey) {
|
|
656
|
+
const descriptor = getLlmProvider(provider);
|
|
657
|
+
if (!descriptor) {
|
|
658
|
+
console.warn(` ! Unknown provider "${provider}". No models available.`);
|
|
659
|
+
return [];
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
return await descriptor.fetchModels(apiKey);
|
|
61
663
|
} catch (e) {
|
|
62
664
|
console.warn(` ! Could not fetch models: ${e.message}. Using defaults.`);
|
|
63
|
-
return
|
|
665
|
+
return descriptor.fallbackModels;
|
|
64
666
|
}
|
|
65
667
|
}
|
|
66
668
|
function pickTarget(options) {
|
|
@@ -140,55 +742,61 @@ async function cmdInit(nameArg, options = {}) {
|
|
|
140
742
|
promptedApiKey = result.apiKey;
|
|
141
743
|
}
|
|
142
744
|
let defaultProviderKey;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
745
|
+
const defaultProviderId = yaml.llm?.provider;
|
|
746
|
+
if (yaml.llm && defaultProviderId) {
|
|
747
|
+
const descriptor = getLlmProvider(defaultProviderId);
|
|
748
|
+
const envKey = descriptor ? process.env[descriptor.envVar] : void 0;
|
|
749
|
+
if (envKey && descriptor) {
|
|
146
750
|
defaultProviderKey = envKey;
|
|
147
|
-
console.log(
|
|
148
|
-
` Using ${yaml.llm.provider === "anthropic" ? "ANTHROPIC" : "OPENAI"}_API_KEY from environment.`
|
|
149
|
-
);
|
|
751
|
+
console.log(` Using ${descriptor.envVar} from environment.`);
|
|
150
752
|
} else if (promptedApiKey) {
|
|
151
753
|
defaultProviderKey = promptedApiKey;
|
|
152
754
|
} else {
|
|
755
|
+
const label = descriptor?.displayName ?? defaultProviderId;
|
|
153
756
|
const { apiKey } = await inquirer.prompt([
|
|
154
757
|
{
|
|
155
758
|
type: "password",
|
|
156
759
|
name: "apiKey",
|
|
157
|
-
message: `${
|
|
760
|
+
message: `${label} API key:`,
|
|
158
761
|
mask: "*"
|
|
159
762
|
}
|
|
160
763
|
]);
|
|
161
764
|
defaultProviderKey = apiKey || void 0;
|
|
162
765
|
}
|
|
163
766
|
}
|
|
164
|
-
|
|
165
|
-
if (yaml.llm && !template) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
767
|
+
const otherProviderKeys = /* @__PURE__ */ new Map();
|
|
768
|
+
if (yaml.llm && !template && defaultProviderId) {
|
|
769
|
+
const otherDescriptors = listLlmProviders().filter(
|
|
770
|
+
(descriptor) => descriptor.id !== defaultProviderId
|
|
771
|
+
);
|
|
772
|
+
for (const descriptor of otherDescriptors) {
|
|
773
|
+
const otherEnvKey = process.env[descriptor.envVar];
|
|
774
|
+
const { configureOther } = await inquirer.prompt([
|
|
775
|
+
{
|
|
776
|
+
type: "confirm",
|
|
777
|
+
name: "configureOther",
|
|
778
|
+
message: `Configure ${descriptor.displayName} API key too (for skills that override the default provider)?`,
|
|
779
|
+
default: Boolean(otherEnvKey)
|
|
780
|
+
}
|
|
781
|
+
]);
|
|
782
|
+
if (!configureOther) {
|
|
783
|
+
continue;
|
|
176
784
|
}
|
|
177
|
-
]);
|
|
178
|
-
if (configureOther) {
|
|
179
785
|
if (otherEnvKey) {
|
|
180
|
-
|
|
181
|
-
console.log(` Using ${
|
|
786
|
+
otherProviderKeys.set(descriptor.id, otherEnvKey);
|
|
787
|
+
console.log(` Using ${descriptor.envVar} from environment.`);
|
|
182
788
|
} else {
|
|
183
789
|
const { promptedOther } = await inquirer.prompt([
|
|
184
790
|
{
|
|
185
791
|
type: "password",
|
|
186
792
|
name: "promptedOther",
|
|
187
|
-
message: `${
|
|
793
|
+
message: `${descriptor.displayName} API key:`,
|
|
188
794
|
mask: "*"
|
|
189
795
|
}
|
|
190
796
|
]);
|
|
191
|
-
|
|
797
|
+
if (promptedOther) {
|
|
798
|
+
otherProviderKeys.set(descriptor.id, promptedOther);
|
|
799
|
+
}
|
|
192
800
|
}
|
|
193
801
|
}
|
|
194
802
|
}
|
|
@@ -224,15 +832,17 @@ async function cmdInit(nameArg, options = {}) {
|
|
|
224
832
|
const nostrPubkey = getPublicKey(nostrSecretBytes);
|
|
225
833
|
const nostrSecretHex = Buffer.from(nostrSecretBytes).toString("hex");
|
|
226
834
|
const created = await createAgentDir({ target, name: agentName, cwd });
|
|
227
|
-
const
|
|
228
|
-
|
|
835
|
+
const collectedKeys = new Map(otherProviderKeys);
|
|
836
|
+
if (yaml.llm && defaultProviderKey) {
|
|
837
|
+
collectedKeys.set(yaml.llm.provider, defaultProviderKey);
|
|
838
|
+
}
|
|
839
|
+
const llmApiKeys = Object.fromEntries(collectedKeys);
|
|
229
840
|
await writeYaml(created.dir, yaml);
|
|
230
841
|
await writeSecrets(
|
|
231
842
|
created.dir,
|
|
232
843
|
{
|
|
233
844
|
nostr_secret_key: nostrSecretHex,
|
|
234
|
-
|
|
235
|
-
openai_api_key: openaiKey
|
|
845
|
+
llm_api_keys: Object.keys(llmApiKeys).length > 0 ? llmApiKeys : void 0
|
|
236
846
|
},
|
|
237
847
|
passphrase || void 0
|
|
238
848
|
);
|
|
@@ -334,8 +944,10 @@ async function promptYaml(inquirer) {
|
|
|
334
944
|
name: "None (non-LLM agent - static-file / static-script / dynamic-script only)",
|
|
335
945
|
value: "none"
|
|
336
946
|
},
|
|
337
|
-
|
|
338
|
-
|
|
947
|
+
...listLlmProviders().map((descriptor2) => ({
|
|
948
|
+
name: descriptor2.displayName,
|
|
949
|
+
value: descriptor2.id
|
|
950
|
+
}))
|
|
339
951
|
]
|
|
340
952
|
}
|
|
341
953
|
]);
|
|
@@ -352,21 +964,25 @@ async function promptYaml(inquirer) {
|
|
|
352
964
|
const yaml2 = ElisymYamlSchema.parse(baseYaml);
|
|
353
965
|
return { yaml: yaml2 };
|
|
354
966
|
}
|
|
355
|
-
const
|
|
967
|
+
const descriptor = getLlmProvider(llmProvider);
|
|
968
|
+
if (!descriptor) {
|
|
969
|
+
throw new Error(`Internal: provider "${llmProvider}" not registered.`);
|
|
970
|
+
}
|
|
971
|
+
const envKey = process.env[descriptor.envVar];
|
|
356
972
|
let apiKey = envKey;
|
|
357
973
|
if (!apiKey) {
|
|
358
974
|
const { promptedKey } = await inquirer.prompt([
|
|
359
975
|
{
|
|
360
976
|
type: "password",
|
|
361
977
|
name: "promptedKey",
|
|
362
|
-
message: `${
|
|
978
|
+
message: `${descriptor.displayName} API key:`,
|
|
363
979
|
mask: "*"
|
|
364
980
|
}
|
|
365
981
|
]);
|
|
366
982
|
apiKey = promptedKey || void 0;
|
|
367
983
|
}
|
|
368
984
|
console.log(" Fetching available models...");
|
|
369
|
-
const models = await
|
|
985
|
+
const models = await fetchModels3(llmProvider, apiKey ?? "");
|
|
370
986
|
const { model } = await inquirer.prompt([
|
|
371
987
|
{
|
|
372
988
|
type: "list",
|
|
@@ -389,18 +1005,17 @@ async function promptYaml(inquirer) {
|
|
|
389
1005
|
});
|
|
390
1006
|
return { yaml, apiKey: envKey ? void 0 : apiKey };
|
|
391
1007
|
}
|
|
392
|
-
var FALLBACK_MODELS;
|
|
393
1008
|
var init_init = __esm({
|
|
394
1009
|
"src/commands/init.ts"() {
|
|
395
|
-
|
|
396
|
-
anthropic: ["claude-sonnet-4-6", "claude-haiku-4-5-20251001", "claude-opus-4-6"],
|
|
397
|
-
openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"]
|
|
398
|
-
};
|
|
1010
|
+
init_llm();
|
|
399
1011
|
}
|
|
400
1012
|
});
|
|
401
1013
|
|
|
402
1014
|
// src/index.ts
|
|
403
1015
|
init_init();
|
|
1016
|
+
|
|
1017
|
+
// src/commands/profile.ts
|
|
1018
|
+
init_llm();
|
|
404
1019
|
async function cmdProfile(name) {
|
|
405
1020
|
const { default: inquirer } = await import('inquirer');
|
|
406
1021
|
const cwd = process.cwd();
|
|
@@ -519,32 +1134,45 @@ async function cmdProfile(name) {
|
|
|
519
1134
|
console.log(" Wallet updated.\n");
|
|
520
1135
|
}
|
|
521
1136
|
if (section === "llm") {
|
|
1137
|
+
const providerChoices = listLlmProviders().map((descriptor) => ({
|
|
1138
|
+
name: descriptor.displayName,
|
|
1139
|
+
value: descriptor.id
|
|
1140
|
+
}));
|
|
1141
|
+
if (providerChoices.length === 0) {
|
|
1142
|
+
console.error(" ! No LLM providers registered.");
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
const firstChoice = providerChoices[0];
|
|
1146
|
+
if (!firstChoice) {
|
|
1147
|
+
console.error(" ! No LLM providers registered.");
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
522
1150
|
const { llmProvider } = await inquirer.prompt([
|
|
523
1151
|
{
|
|
524
1152
|
type: "list",
|
|
525
1153
|
name: "llmProvider",
|
|
526
1154
|
message: "Default LLM provider (used by skills without a `provider:` override):",
|
|
527
|
-
choices:
|
|
528
|
-
|
|
529
|
-
{ name: "OpenAI (GPT)", value: "openai" }
|
|
530
|
-
],
|
|
531
|
-
default: loaded.yaml.llm?.provider ?? "anthropic"
|
|
1155
|
+
choices: providerChoices,
|
|
1156
|
+
default: loaded.yaml.llm?.provider ?? firstChoice.value
|
|
532
1157
|
}
|
|
533
1158
|
]);
|
|
534
|
-
const
|
|
1159
|
+
const defaultDescriptor = getLlmProvider(llmProvider);
|
|
1160
|
+
if (!defaultDescriptor) {
|
|
1161
|
+
throw new Error(`Internal: provider "${llmProvider}" not registered.`);
|
|
1162
|
+
}
|
|
535
1163
|
const defaultKeyStatus = describeKeyStatus(loaded.secrets, llmProvider);
|
|
536
1164
|
const { apiKey } = await inquirer.prompt([
|
|
537
1165
|
{
|
|
538
1166
|
type: "password",
|
|
539
1167
|
name: "apiKey",
|
|
540
|
-
message: `${
|
|
1168
|
+
message: `${defaultDescriptor.displayName} API key [${defaultKeyStatus}] (leave empty to keep current):`,
|
|
541
1169
|
mask: "*"
|
|
542
1170
|
}
|
|
543
1171
|
]);
|
|
544
1172
|
const probeKey = apiKey || pickPlainKey(loaded.secrets, llmProvider);
|
|
545
1173
|
console.log(" Fetching available models...");
|
|
546
|
-
const { fetchModels:
|
|
547
|
-
const models = await
|
|
1174
|
+
const { fetchModels: fetchModels4 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1175
|
+
const models = await fetchModels4(llmProvider, probeKey);
|
|
548
1176
|
const { model } = await inquirer.prompt([
|
|
549
1177
|
{
|
|
550
1178
|
type: "list",
|
|
@@ -562,35 +1190,42 @@ async function cmdProfile(name) {
|
|
|
562
1190
|
default: loaded.yaml.llm?.max_tokens ?? 4096
|
|
563
1191
|
}
|
|
564
1192
|
]);
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1193
|
+
const otherDescriptors = listLlmProviders().filter(
|
|
1194
|
+
(descriptor) => descriptor.id !== llmProvider
|
|
1195
|
+
);
|
|
1196
|
+
const otherKeys = /* @__PURE__ */ new Map();
|
|
1197
|
+
for (const descriptor of otherDescriptors) {
|
|
1198
|
+
const status = describeKeyStatus(loaded.secrets, descriptor.id);
|
|
1199
|
+
const { otherApiKey } = await inquirer.prompt([
|
|
1200
|
+
{
|
|
1201
|
+
type: "password",
|
|
1202
|
+
name: "otherApiKey",
|
|
1203
|
+
message: `${descriptor.displayName} API key for skill overrides [${status}] (leave empty to keep current):`,
|
|
1204
|
+
mask: "*"
|
|
1205
|
+
}
|
|
1206
|
+
]);
|
|
1207
|
+
if (otherApiKey) {
|
|
1208
|
+
otherKeys.set(descriptor.id, otherApiKey);
|
|
572
1209
|
}
|
|
573
|
-
|
|
1210
|
+
}
|
|
574
1211
|
const nextLlm = { provider: llmProvider, model, max_tokens: maxTokens };
|
|
575
1212
|
const nextYaml = { ...loaded.yaml, llm: nextLlm };
|
|
576
1213
|
await writeYaml(loaded.dir, nextYaml);
|
|
577
1214
|
loaded.yaml = nextYaml;
|
|
578
|
-
if (apiKey ||
|
|
579
|
-
const
|
|
1215
|
+
if (apiKey || otherKeys.size > 0) {
|
|
1216
|
+
const nextLlmApiKeys = {
|
|
1217
|
+
...loaded.secrets.llm_api_keys ?? {}
|
|
1218
|
+
};
|
|
580
1219
|
if (apiKey) {
|
|
581
|
-
|
|
582
|
-
nextSecrets.anthropic_api_key = apiKey;
|
|
583
|
-
} else {
|
|
584
|
-
nextSecrets.openai_api_key = apiKey;
|
|
585
|
-
}
|
|
1220
|
+
nextLlmApiKeys[llmProvider] = apiKey;
|
|
586
1221
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
nextSecrets.anthropic_api_key = otherApiKey;
|
|
590
|
-
} else {
|
|
591
|
-
nextSecrets.openai_api_key = otherApiKey;
|
|
592
|
-
}
|
|
1222
|
+
for (const [providerId, key] of otherKeys) {
|
|
1223
|
+
nextLlmApiKeys[providerId] = key;
|
|
593
1224
|
}
|
|
1225
|
+
const nextSecrets = {
|
|
1226
|
+
...loaded.secrets,
|
|
1227
|
+
llm_api_keys: nextLlmApiKeys
|
|
1228
|
+
};
|
|
594
1229
|
await writeSecrets(loaded.dir, nextSecrets, passphrase);
|
|
595
1230
|
loaded.secrets = nextSecrets;
|
|
596
1231
|
}
|
|
@@ -606,16 +1241,12 @@ function truncate(value, max = 40) {
|
|
|
606
1241
|
}
|
|
607
1242
|
return value.slice(0, max - 1) + "\u2026";
|
|
608
1243
|
}
|
|
609
|
-
function
|
|
610
|
-
return
|
|
611
|
-
}
|
|
612
|
-
function describeKeyStatus(secrets, provider) {
|
|
613
|
-
const perProviderField = provider === "anthropic" ? "anthropic_api_key" : "openai_api_key";
|
|
614
|
-
return secrets[perProviderField] ? "set" : "not set";
|
|
1244
|
+
function describeKeyStatus(secrets, providerId) {
|
|
1245
|
+
return secrets.llm_api_keys?.[providerId] ? "set" : "not set";
|
|
615
1246
|
}
|
|
616
|
-
function pickPlainKey(secrets,
|
|
617
|
-
const
|
|
618
|
-
return
|
|
1247
|
+
function pickPlainKey(secrets, providerId) {
|
|
1248
|
+
const value = secrets.llm_api_keys?.[providerId];
|
|
1249
|
+
return typeof value === "string" ? value : "";
|
|
619
1250
|
}
|
|
620
1251
|
var TCP_PROBE_TIMEOUT_MS = 3e3;
|
|
621
1252
|
function parseRelayUrl(url) {
|
|
@@ -875,322 +1506,11 @@ var JobLedger = class {
|
|
|
875
1506
|
}
|
|
876
1507
|
};
|
|
877
1508
|
|
|
878
|
-
// src/
|
|
879
|
-
|
|
880
|
-
var MAX_RETRIES = 2;
|
|
881
|
-
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
882
|
-
function createAbortError() {
|
|
883
|
-
const err = new Error("The operation was aborted");
|
|
884
|
-
err.name = "AbortError";
|
|
885
|
-
return err;
|
|
886
|
-
}
|
|
887
|
-
function sleepWithSignal(ms, signal) {
|
|
888
|
-
if (signal?.aborted) {
|
|
889
|
-
throw createAbortError();
|
|
890
|
-
}
|
|
891
|
-
if (!signal) {
|
|
892
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
893
|
-
}
|
|
894
|
-
return new Promise((resolve3, reject) => {
|
|
895
|
-
const cleanup = () => {
|
|
896
|
-
clearTimeout(timer);
|
|
897
|
-
signal.removeEventListener("abort", onAbort);
|
|
898
|
-
};
|
|
899
|
-
const onAbort = () => {
|
|
900
|
-
cleanup();
|
|
901
|
-
reject(createAbortError());
|
|
902
|
-
};
|
|
903
|
-
const timer = setTimeout(() => {
|
|
904
|
-
cleanup();
|
|
905
|
-
resolve3();
|
|
906
|
-
}, ms);
|
|
907
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
async function fetchWithTimeout(url, init, signal) {
|
|
911
|
-
if (signal?.aborted) {
|
|
912
|
-
throw createAbortError();
|
|
913
|
-
}
|
|
914
|
-
const controller = new AbortController();
|
|
915
|
-
const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
|
|
916
|
-
const onAbort = () => controller.abort();
|
|
917
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
918
|
-
try {
|
|
919
|
-
return await fetch(url, { ...init, signal: controller.signal });
|
|
920
|
-
} finally {
|
|
921
|
-
clearTimeout(timer);
|
|
922
|
-
signal?.removeEventListener("abort", onAbort);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
async function fetchWithRetry(url, init, signal) {
|
|
926
|
-
for (let attempt = 0; ; attempt++) {
|
|
927
|
-
let res;
|
|
928
|
-
try {
|
|
929
|
-
res = await fetchWithTimeout(url, init, signal);
|
|
930
|
-
} catch (e) {
|
|
931
|
-
if (attempt >= MAX_RETRIES || e.name === "AbortError") {
|
|
932
|
-
throw e;
|
|
933
|
-
}
|
|
934
|
-
await sleepWithSignal(Math.min(1e3 * 2 ** attempt, 8e3), signal);
|
|
935
|
-
continue;
|
|
936
|
-
}
|
|
937
|
-
if (res.ok || attempt >= MAX_RETRIES || !RETRYABLE_STATUSES.has(res.status)) {
|
|
938
|
-
return res;
|
|
939
|
-
}
|
|
940
|
-
const retryAfter = res.headers.get("retry-after");
|
|
941
|
-
const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3 || 1e3 * 2 ** attempt, 3e4) : Math.min(1e3 * 2 ** attempt, 8e3);
|
|
942
|
-
await sleepWithSignal(delay, signal);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
function isReasoningModel(model) {
|
|
946
|
-
return /^o\d/.test(model) || /^gpt-5(\b|[-.])/.test(model);
|
|
947
|
-
}
|
|
948
|
-
function createLlmClient(config) {
|
|
949
|
-
if (config.provider === "anthropic") {
|
|
950
|
-
return new AnthropicClient(config);
|
|
951
|
-
}
|
|
952
|
-
return new OpenAIClient(config);
|
|
953
|
-
}
|
|
954
|
-
async function verifyLlmApiKey(provider, apiKey, signal) {
|
|
955
|
-
const url = provider === "anthropic" ? "https://api.anthropic.com/v1/models?limit=1" : "https://api.openai.com/v1/models";
|
|
956
|
-
const headers = provider === "anthropic" ? { "x-api-key": apiKey, "anthropic-version": "2023-06-01" } : { Authorization: `Bearer ${apiKey}` };
|
|
957
|
-
try {
|
|
958
|
-
const res = await fetchWithTimeout(url, { method: "GET", headers }, signal);
|
|
959
|
-
if (res.ok) {
|
|
960
|
-
await res.body?.cancel().catch(() => void 0);
|
|
961
|
-
return { ok: true };
|
|
962
|
-
}
|
|
963
|
-
const body = (await res.text().catch(() => "")).slice(0, 500);
|
|
964
|
-
if (res.status === 401 || res.status === 403) {
|
|
965
|
-
return { ok: false, reason: "invalid", status: res.status, body };
|
|
966
|
-
}
|
|
967
|
-
return { ok: false, reason: "unavailable", error: `HTTP ${res.status}: ${body.slice(0, 200)}` };
|
|
968
|
-
} catch (e) {
|
|
969
|
-
return { ok: false, reason: "unavailable", error: e?.message ?? String(e) };
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
var AnthropicClient = class {
|
|
973
|
-
constructor(config) {
|
|
974
|
-
this.config = config;
|
|
975
|
-
}
|
|
976
|
-
totalIn = 0;
|
|
977
|
-
totalOut = 0;
|
|
978
|
-
logTokens(usage) {
|
|
979
|
-
if (this.config.logUsage && usage) {
|
|
980
|
-
this.totalIn += usage.input_tokens ?? 0;
|
|
981
|
-
this.totalOut += usage.output_tokens ?? 0;
|
|
982
|
-
console.log(
|
|
983
|
-
` [LLM] ${this.config.model} tokens: in=${usage.input_tokens ?? 0} out=${usage.output_tokens ?? 0} (total: in=${this.totalIn} out=${this.totalOut})`
|
|
984
|
-
);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
async complete(systemPrompt, userInput, signal) {
|
|
988
|
-
const res = await fetchWithRetry(
|
|
989
|
-
"https://api.anthropic.com/v1/messages",
|
|
990
|
-
{
|
|
991
|
-
method: "POST",
|
|
992
|
-
headers: {
|
|
993
|
-
"Content-Type": "application/json",
|
|
994
|
-
"x-api-key": this.config.apiKey,
|
|
995
|
-
"anthropic-version": "2023-06-01"
|
|
996
|
-
},
|
|
997
|
-
body: JSON.stringify({
|
|
998
|
-
model: this.config.model,
|
|
999
|
-
max_tokens: this.config.maxTokens,
|
|
1000
|
-
system: systemPrompt,
|
|
1001
|
-
messages: [{ role: "user", content: userInput }]
|
|
1002
|
-
})
|
|
1003
|
-
},
|
|
1004
|
-
signal
|
|
1005
|
-
);
|
|
1006
|
-
if (!res.ok) {
|
|
1007
|
-
throw new Error(`Anthropic API error: ${res.status} ${await res.text()}`);
|
|
1008
|
-
}
|
|
1009
|
-
const data = await res.json();
|
|
1010
|
-
this.logTokens(data.usage);
|
|
1011
|
-
const textBlock = data.content?.find((b) => b.type === "text");
|
|
1012
|
-
return textBlock?.text ?? "";
|
|
1013
|
-
}
|
|
1014
|
-
async completeWithTools(systemPrompt, messages, tools, signal) {
|
|
1015
|
-
const anthropicTools = tools.map((t) => ({
|
|
1016
|
-
name: t.name,
|
|
1017
|
-
description: t.description,
|
|
1018
|
-
input_schema: {
|
|
1019
|
-
type: "object",
|
|
1020
|
-
properties: Object.fromEntries(
|
|
1021
|
-
t.parameters.map((p) => [p.name, { type: "string", description: p.description }])
|
|
1022
|
-
),
|
|
1023
|
-
required: t.parameters.filter((p) => p.required).map((p) => p.name)
|
|
1024
|
-
}
|
|
1025
|
-
}));
|
|
1026
|
-
const res = await fetchWithRetry(
|
|
1027
|
-
"https://api.anthropic.com/v1/messages",
|
|
1028
|
-
{
|
|
1029
|
-
method: "POST",
|
|
1030
|
-
headers: {
|
|
1031
|
-
"Content-Type": "application/json",
|
|
1032
|
-
"x-api-key": this.config.apiKey,
|
|
1033
|
-
"anthropic-version": "2023-06-01"
|
|
1034
|
-
},
|
|
1035
|
-
body: JSON.stringify({
|
|
1036
|
-
model: this.config.model,
|
|
1037
|
-
max_tokens: this.config.maxTokens,
|
|
1038
|
-
system: systemPrompt,
|
|
1039
|
-
messages,
|
|
1040
|
-
tools: anthropicTools
|
|
1041
|
-
})
|
|
1042
|
-
},
|
|
1043
|
-
signal
|
|
1044
|
-
);
|
|
1045
|
-
if (!res.ok) {
|
|
1046
|
-
throw new Error(`Anthropic API error: ${res.status} ${await res.text()}`);
|
|
1047
|
-
}
|
|
1048
|
-
const data = await res.json();
|
|
1049
|
-
this.logTokens(data.usage);
|
|
1050
|
-
const content = data.content ?? [];
|
|
1051
|
-
const toolUses = content.filter((b) => b.type === "tool_use");
|
|
1052
|
-
if (toolUses.length > 0) {
|
|
1053
|
-
const calls = toolUses.map((t) => ({
|
|
1054
|
-
id: t.id,
|
|
1055
|
-
name: t.name,
|
|
1056
|
-
arguments: t.input ?? {}
|
|
1057
|
-
}));
|
|
1058
|
-
return {
|
|
1059
|
-
type: "tool_use",
|
|
1060
|
-
calls,
|
|
1061
|
-
assistantMessage: { role: "assistant", content }
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
const textBlock = content.find((b) => b.type === "text");
|
|
1065
|
-
return { type: "text", text: textBlock?.text ?? "" };
|
|
1066
|
-
}
|
|
1067
|
-
formatToolResultMessages(results) {
|
|
1068
|
-
return [
|
|
1069
|
-
{
|
|
1070
|
-
role: "user",
|
|
1071
|
-
content: results.map((r) => ({
|
|
1072
|
-
type: "tool_result",
|
|
1073
|
-
tool_use_id: r.callId,
|
|
1074
|
-
content: r.content
|
|
1075
|
-
}))
|
|
1076
|
-
}
|
|
1077
|
-
];
|
|
1078
|
-
}
|
|
1079
|
-
};
|
|
1080
|
-
var OpenAIClient = class {
|
|
1081
|
-
constructor(config) {
|
|
1082
|
-
this.config = config;
|
|
1083
|
-
}
|
|
1084
|
-
totalIn = 0;
|
|
1085
|
-
totalOut = 0;
|
|
1086
|
-
logTokens(usage) {
|
|
1087
|
-
if (this.config.logUsage && usage) {
|
|
1088
|
-
this.totalIn += usage.prompt_tokens ?? 0;
|
|
1089
|
-
this.totalOut += usage.completion_tokens ?? 0;
|
|
1090
|
-
console.log(
|
|
1091
|
-
` [LLM] ${this.config.model} tokens: in=${usage.prompt_tokens ?? 0} out=${usage.completion_tokens ?? 0} (total: in=${this.totalIn} out=${this.totalOut})`
|
|
1092
|
-
);
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
async complete(systemPrompt, userInput, signal) {
|
|
1096
|
-
const isReasoning = isReasoningModel(this.config.model);
|
|
1097
|
-
const res = await fetchWithRetry(
|
|
1098
|
-
"https://api.openai.com/v1/chat/completions",
|
|
1099
|
-
{
|
|
1100
|
-
method: "POST",
|
|
1101
|
-
headers: {
|
|
1102
|
-
"Content-Type": "application/json",
|
|
1103
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
1104
|
-
},
|
|
1105
|
-
body: JSON.stringify({
|
|
1106
|
-
model: this.config.model,
|
|
1107
|
-
...isReasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
|
|
1108
|
-
messages: [
|
|
1109
|
-
{ role: isReasoning ? "developer" : "system", content: systemPrompt },
|
|
1110
|
-
{ role: "user", content: userInput }
|
|
1111
|
-
]
|
|
1112
|
-
})
|
|
1113
|
-
},
|
|
1114
|
-
signal
|
|
1115
|
-
);
|
|
1116
|
-
if (!res.ok) {
|
|
1117
|
-
throw new Error(`OpenAI API error: ${res.status} ${await res.text()}`);
|
|
1118
|
-
}
|
|
1119
|
-
const data = await res.json();
|
|
1120
|
-
this.logTokens(data.usage);
|
|
1121
|
-
return data.choices?.[0]?.message?.content ?? "";
|
|
1122
|
-
}
|
|
1123
|
-
async completeWithTools(systemPrompt, messages, tools, signal) {
|
|
1124
|
-
const openaiTools = tools.map((t) => ({
|
|
1125
|
-
type: "function",
|
|
1126
|
-
function: {
|
|
1127
|
-
name: t.name,
|
|
1128
|
-
description: t.description,
|
|
1129
|
-
parameters: {
|
|
1130
|
-
type: "object",
|
|
1131
|
-
properties: Object.fromEntries(
|
|
1132
|
-
t.parameters.map((p) => [p.name, { type: "string", description: p.description }])
|
|
1133
|
-
),
|
|
1134
|
-
required: t.parameters.filter((p) => p.required).map((p) => p.name)
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
}));
|
|
1138
|
-
const isReasoning = isReasoningModel(this.config.model);
|
|
1139
|
-
const res = await fetchWithRetry(
|
|
1140
|
-
"https://api.openai.com/v1/chat/completions",
|
|
1141
|
-
{
|
|
1142
|
-
method: "POST",
|
|
1143
|
-
headers: {
|
|
1144
|
-
"Content-Type": "application/json",
|
|
1145
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
1146
|
-
},
|
|
1147
|
-
body: JSON.stringify({
|
|
1148
|
-
model: this.config.model,
|
|
1149
|
-
...isReasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
|
|
1150
|
-
messages: [
|
|
1151
|
-
{ role: isReasoning ? "developer" : "system", content: systemPrompt },
|
|
1152
|
-
...messages
|
|
1153
|
-
],
|
|
1154
|
-
tools: openaiTools
|
|
1155
|
-
})
|
|
1156
|
-
},
|
|
1157
|
-
signal
|
|
1158
|
-
);
|
|
1159
|
-
if (!res.ok) {
|
|
1160
|
-
throw new Error(`OpenAI API error: ${res.status} ${await res.text()}`);
|
|
1161
|
-
}
|
|
1162
|
-
const data = await res.json();
|
|
1163
|
-
this.logTokens(data.usage);
|
|
1164
|
-
const message = data.choices?.[0]?.message;
|
|
1165
|
-
if (message?.tool_calls?.length > 0) {
|
|
1166
|
-
const calls = message.tool_calls.map((tc) => {
|
|
1167
|
-
let args;
|
|
1168
|
-
try {
|
|
1169
|
-
args = JSON.parse(tc.function.arguments ?? "{}");
|
|
1170
|
-
} catch {
|
|
1171
|
-
args = {};
|
|
1172
|
-
}
|
|
1173
|
-
return { id: tc.id, name: tc.function.name, arguments: args };
|
|
1174
|
-
});
|
|
1175
|
-
return {
|
|
1176
|
-
type: "tool_use",
|
|
1177
|
-
calls,
|
|
1178
|
-
assistantMessage: message
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
return { type: "text", text: message?.content ?? "" };
|
|
1182
|
-
}
|
|
1183
|
-
formatToolResultMessages(results) {
|
|
1184
|
-
return results.map((r) => ({
|
|
1185
|
-
role: "tool",
|
|
1186
|
-
tool_call_id: r.callId,
|
|
1187
|
-
content: r.content
|
|
1188
|
-
}));
|
|
1189
|
-
}
|
|
1190
|
-
};
|
|
1509
|
+
// src/commands/start.ts
|
|
1510
|
+
init_llm();
|
|
1191
1511
|
|
|
1192
1512
|
// src/llm/resolve.ts
|
|
1193
|
-
var
|
|
1513
|
+
var DEFAULT_MAX_TOKENS3 = 4096;
|
|
1194
1514
|
function resolveSkillLlm(input, agentDefault) {
|
|
1195
1515
|
const override = input.llmOverride;
|
|
1196
1516
|
const overridePairSet = override?.provider !== void 0 && override.model !== void 0;
|
|
@@ -1208,7 +1528,7 @@ function resolveSkillLlm(input, agentDefault) {
|
|
|
1208
1528
|
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>'.`
|
|
1209
1529
|
};
|
|
1210
1530
|
}
|
|
1211
|
-
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ??
|
|
1531
|
+
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS3;
|
|
1212
1532
|
return { provider, model, maxTokens };
|
|
1213
1533
|
}
|
|
1214
1534
|
|
|
@@ -1234,34 +1554,32 @@ function resolveTripleForOverride(override, agentDefault) {
|
|
|
1234
1554
|
if (!provider || !model) {
|
|
1235
1555
|
return void 0;
|
|
1236
1556
|
}
|
|
1237
|
-
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ??
|
|
1557
|
+
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS3;
|
|
1238
1558
|
return { provider, model, maxTokens };
|
|
1239
1559
|
}
|
|
1240
1560
|
|
|
1241
1561
|
// src/llm/keys.ts
|
|
1242
|
-
|
|
1243
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
1244
|
-
openai: "OPENAI_API_KEY"
|
|
1245
|
-
};
|
|
1246
|
-
var SECRET_FIELD_FOR_PROVIDER = {
|
|
1247
|
-
anthropic: "anthropic_api_key",
|
|
1248
|
-
openai: "openai_api_key"
|
|
1249
|
-
};
|
|
1562
|
+
init_registry();
|
|
1250
1563
|
function resolveProviderApiKey(input) {
|
|
1251
1564
|
const { provider, secrets, dependentSkills } = input;
|
|
1252
|
-
const
|
|
1253
|
-
|
|
1254
|
-
|
|
1565
|
+
const descriptor = getLlmProvider(provider);
|
|
1566
|
+
if (!descriptor) {
|
|
1567
|
+
const skillList2 = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
|
|
1568
|
+
return {
|
|
1569
|
+
error: `Provider "${provider}" is not registered (required by skill(s): ${skillList2}).`
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const perProviderValue = secrets.llm_api_keys?.[provider];
|
|
1573
|
+
if (typeof perProviderValue === "string" && perProviderValue.length > 0) {
|
|
1255
1574
|
return { apiKey: perProviderValue, origin: "secrets-per-provider" };
|
|
1256
1575
|
}
|
|
1257
|
-
const
|
|
1258
|
-
const envValue = process.env[envVar];
|
|
1576
|
+
const envValue = process.env[descriptor.envVar];
|
|
1259
1577
|
if (envValue) {
|
|
1260
1578
|
return { apiKey: envValue, origin: "env" };
|
|
1261
1579
|
}
|
|
1262
1580
|
const skillList = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
|
|
1263
1581
|
return {
|
|
1264
|
-
error: `Provider "${provider}" needs an API key (required by skill(s): ${skillList}). Set secrets.${
|
|
1582
|
+
error: `Provider "${provider}" needs an API key (required by skill(s): ${skillList}). Set secrets.llm_api_keys.${provider} via 'npx @elisym/cli profile <agent>' or export ${descriptor.envVar}.`
|
|
1265
1583
|
};
|
|
1266
1584
|
}
|
|
1267
1585
|
function resolveLevel(options) {
|
|
@@ -1330,13 +1648,14 @@ var GLOBAL_MAX_JOBS_PER_WINDOW = 200;
|
|
|
1330
1648
|
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
1331
1649
|
var GLOBAL_LIMITER_KEY = "__global__";
|
|
1332
1650
|
var AgentRuntime = class {
|
|
1333
|
-
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}) {
|
|
1651
|
+
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor) {
|
|
1334
1652
|
this.transport = transport;
|
|
1335
1653
|
this.skills = skills;
|
|
1336
1654
|
this.skillCtx = skillCtx;
|
|
1337
1655
|
this.config = config;
|
|
1338
1656
|
this.ledger = ledger;
|
|
1339
1657
|
this.callbacks = callbacks;
|
|
1658
|
+
this.healthMonitor = healthMonitor;
|
|
1340
1659
|
this.limit = pLimit(config.maxConcurrentJobs);
|
|
1341
1660
|
this.maxQueueSize = config.maxQueueSize ?? config.maxConcurrentJobs * 10;
|
|
1342
1661
|
}
|
|
@@ -1361,6 +1680,13 @@ var AgentRuntime = class {
|
|
|
1361
1680
|
maxPerWindow: GLOBAL_MAX_JOBS_PER_WINDOW,
|
|
1362
1681
|
maxKeys: 1
|
|
1363
1682
|
});
|
|
1683
|
+
/**
|
|
1684
|
+
* Two-tier limiter applied only to free LLM skills (mode='llm', price=0).
|
|
1685
|
+
* Existing per-customer + global limiters above remain in effect for all
|
|
1686
|
+
* jobs; this set adds tighter caps to prevent unpaid spam from draining
|
|
1687
|
+
* the operator's API key.
|
|
1688
|
+
*/
|
|
1689
|
+
freeLlmLimiters = createFreeLlmLimiterSet();
|
|
1364
1690
|
/** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
|
|
1365
1691
|
async fetchProtocolConfig() {
|
|
1366
1692
|
if (this.config.network !== "devnet") {
|
|
@@ -1417,8 +1743,33 @@ var AgentRuntime = class {
|
|
|
1417
1743
|
});
|
|
1418
1744
|
return;
|
|
1419
1745
|
}
|
|
1746
|
+
const matched = this.skills.route(job.tags);
|
|
1747
|
+
const isFreeLlm = matched?.mode === "llm" && matched.priceSubunits === 0;
|
|
1748
|
+
let perCustomerLimiter;
|
|
1749
|
+
let perSkillKey;
|
|
1750
|
+
if (isFreeLlm && matched) {
|
|
1751
|
+
if (!this.freeLlmLimiters.globalLimiter.peek(FREE_LLM_GLOBAL_KEY).allowed) {
|
|
1752
|
+
this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
|
|
1753
|
+
});
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
perCustomerLimiter = this.freeLlmLimiters.getPerCustomerLimiter(
|
|
1757
|
+
matched.name,
|
|
1758
|
+
matched.rateLimit
|
|
1759
|
+
);
|
|
1760
|
+
perSkillKey = freeLlmCustomerKey(job.customerId, matched.name);
|
|
1761
|
+
if (!perCustomerLimiter.peek(perSkillKey).allowed) {
|
|
1762
|
+
this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
|
|
1763
|
+
});
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1420
1767
|
this.customerLimiter.check(job.customerId);
|
|
1421
1768
|
this.globalLimiter.check(GLOBAL_LIMITER_KEY);
|
|
1769
|
+
if (isFreeLlm && perCustomerLimiter && perSkillKey) {
|
|
1770
|
+
this.freeLlmLimiters.globalLimiter.check(FREE_LLM_GLOBAL_KEY);
|
|
1771
|
+
perCustomerLimiter.check(perSkillKey);
|
|
1772
|
+
}
|
|
1422
1773
|
this.callbacks.onJobReceived?.(job);
|
|
1423
1774
|
this.inFlight.add(job.jobId);
|
|
1424
1775
|
this.pending++;
|
|
@@ -1446,10 +1797,12 @@ var AgentRuntime = class {
|
|
|
1446
1797
|
process.on("SIGTERM", onSignal);
|
|
1447
1798
|
});
|
|
1448
1799
|
}
|
|
1449
|
-
/** Drop expired hits from
|
|
1800
|
+
/** Drop expired hits from every sliding-window limiter. */
|
|
1450
1801
|
cleanupRateLimits() {
|
|
1451
1802
|
this.customerLimiter.prune();
|
|
1452
1803
|
this.globalLimiter.prune();
|
|
1804
|
+
this.freeLlmLimiters.globalLimiter.prune();
|
|
1805
|
+
this.freeLlmLimiters.prunePerCustomer();
|
|
1453
1806
|
}
|
|
1454
1807
|
stop() {
|
|
1455
1808
|
if (this.stopped) {
|
|
@@ -1513,6 +1866,24 @@ var AgentRuntime = class {
|
|
|
1513
1866
|
if (job.input.length > LIMITS.MAX_INPUT_LENGTH) {
|
|
1514
1867
|
throw new Error(`Input too long: ${job.input.length} chars (max ${LIMITS.MAX_INPUT_LENGTH})`);
|
|
1515
1868
|
}
|
|
1869
|
+
const matched = this.skills.route(job.tags);
|
|
1870
|
+
if (this.healthMonitor && matched && matched.mode === "llm" && matched.resolvedTriple) {
|
|
1871
|
+
try {
|
|
1872
|
+
await this.healthMonitor.assertReady(
|
|
1873
|
+
matched.resolvedTriple.provider,
|
|
1874
|
+
matched.resolvedTriple.model
|
|
1875
|
+
);
|
|
1876
|
+
} catch (err) {
|
|
1877
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1878
|
+
log(`[${job.jobId.slice(0, 8)}] LLM health gate refused job: ${detail}`);
|
|
1879
|
+
await this.transport.sendFeedback(job, {
|
|
1880
|
+
type: "error",
|
|
1881
|
+
message: "Service temporarily unavailable, try again later"
|
|
1882
|
+
}).catch(() => {
|
|
1883
|
+
});
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1516
1887
|
const jobPrice = resolveJobPrice(job.tags, this.skills);
|
|
1517
1888
|
const jobAsset = resolveJobAsset(job.tags, this.skills);
|
|
1518
1889
|
let netAmount;
|
|
@@ -2109,9 +2480,10 @@ var ScriptSkill = class {
|
|
|
2109
2480
|
|
|
2110
2481
|
// src/skill/loader.ts
|
|
2111
2482
|
function buildCliSkill(parsed, entryPath) {
|
|
2483
|
+
let skill;
|
|
2112
2484
|
switch (parsed.mode) {
|
|
2113
2485
|
case "llm":
|
|
2114
|
-
|
|
2486
|
+
skill = new ScriptSkill(
|
|
2115
2487
|
parsed.name,
|
|
2116
2488
|
parsed.description,
|
|
2117
2489
|
parsed.capabilities,
|
|
@@ -2125,6 +2497,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2125
2497
|
parsed.maxToolRounds,
|
|
2126
2498
|
parsed.llmOverride
|
|
2127
2499
|
);
|
|
2500
|
+
break;
|
|
2128
2501
|
case "static-file": {
|
|
2129
2502
|
if (parsed.outputFile === void 0) {
|
|
2130
2503
|
throw new Error(
|
|
@@ -2137,7 +2510,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2137
2510
|
`SKILL.md "${parsed.name}": "output_file" must stay inside the skill directory`
|
|
2138
2511
|
);
|
|
2139
2512
|
}
|
|
2140
|
-
|
|
2513
|
+
skill = new StaticFileSkill({
|
|
2141
2514
|
name: parsed.name,
|
|
2142
2515
|
description: parsed.description,
|
|
2143
2516
|
capabilities: parsed.capabilities,
|
|
@@ -2148,6 +2521,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2148
2521
|
imageFile: parsed.imageFile,
|
|
2149
2522
|
dir: entryPath
|
|
2150
2523
|
});
|
|
2524
|
+
break;
|
|
2151
2525
|
}
|
|
2152
2526
|
case "static-script":
|
|
2153
2527
|
case "dynamic-script": {
|
|
@@ -2161,7 +2535,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2161
2535
|
throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
|
|
2162
2536
|
}
|
|
2163
2537
|
const Ctor = parsed.mode === "static-script" ? StaticScriptSkill : DynamicScriptSkill;
|
|
2164
|
-
|
|
2538
|
+
skill = new Ctor({
|
|
2165
2539
|
name: parsed.name,
|
|
2166
2540
|
description: parsed.description,
|
|
2167
2541
|
capabilities: parsed.capabilities,
|
|
@@ -2174,8 +2548,13 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2174
2548
|
imageFile: parsed.imageFile,
|
|
2175
2549
|
dir: entryPath
|
|
2176
2550
|
});
|
|
2551
|
+
break;
|
|
2177
2552
|
}
|
|
2178
2553
|
}
|
|
2554
|
+
if (parsed.rateLimit) {
|
|
2555
|
+
skill.rateLimit = parsed.rateLimit;
|
|
2556
|
+
}
|
|
2557
|
+
return skill;
|
|
2179
2558
|
}
|
|
2180
2559
|
function loadSkillsFromDir(skillsDir) {
|
|
2181
2560
|
const skills = [];
|
|
@@ -2581,6 +2960,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2581
2960
|
const dependentSkillsByProvider = /* @__PURE__ */ new Map();
|
|
2582
2961
|
let agentDefaultCacheKey;
|
|
2583
2962
|
const llmClientCache = /* @__PURE__ */ new Map();
|
|
2963
|
+
const healthMonitor = new LlmHealthMonitor();
|
|
2584
2964
|
if (llmSkills.length > 0) {
|
|
2585
2965
|
const resolutionErrors = [];
|
|
2586
2966
|
for (const skill of llmSkills) {
|
|
@@ -2595,6 +2975,11 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2595
2975
|
}
|
|
2596
2976
|
const cacheKey = cacheKeyFor(result);
|
|
2597
2977
|
triplesByKey.set(cacheKey, result);
|
|
2978
|
+
skill.resolvedTriple = {
|
|
2979
|
+
provider: result.provider,
|
|
2980
|
+
model: result.model,
|
|
2981
|
+
maxTokens: result.maxTokens
|
|
2982
|
+
};
|
|
2598
2983
|
const list = dependentSkillsByProvider.get(result.provider) ?? [];
|
|
2599
2984
|
list.push(skill.name);
|
|
2600
2985
|
dependentSkillsByProvider.set(result.provider, list);
|
|
@@ -2638,28 +3023,47 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2638
3023
|
console.error("");
|
|
2639
3024
|
process.exit(1);
|
|
2640
3025
|
}
|
|
2641
|
-
for (const
|
|
2642
|
-
const apiKey = keyByProvider.get(provider);
|
|
3026
|
+
for (const [, triple] of triplesByKey) {
|
|
3027
|
+
const apiKey = keyByProvider.get(triple.provider);
|
|
2643
3028
|
if (!apiKey) {
|
|
2644
3029
|
continue;
|
|
2645
3030
|
}
|
|
2646
|
-
const envVar = provider
|
|
2647
|
-
process.stdout.write(` Verifying ${provider}
|
|
2648
|
-
const verification = await
|
|
3031
|
+
const envVar = getLlmProvider(triple.provider)?.envVar ?? `${triple.provider.toUpperCase()}_API_KEY`;
|
|
3032
|
+
process.stdout.write(` Verifying ${triple.provider} ${triple.model}... `);
|
|
3033
|
+
const verification = await verifyLlmApiKeyDeep(triple.provider, apiKey, triple.model);
|
|
3034
|
+
const descriptor = getLlmProvider(triple.provider);
|
|
3035
|
+
const verifyFn = async (signal) => descriptor ? descriptor.verifyKeyDeep(apiKey, triple.model, signal) : { ok: false, reason: "unavailable", error: "no descriptor" };
|
|
3036
|
+
healthMonitor.register({
|
|
3037
|
+
provider: triple.provider,
|
|
3038
|
+
model: triple.model,
|
|
3039
|
+
verifyFn
|
|
3040
|
+
});
|
|
3041
|
+
healthMonitor.seed(triple.provider, triple.model, verification);
|
|
2649
3042
|
if (verification.ok) {
|
|
2650
3043
|
console.log("ok");
|
|
2651
3044
|
} else if (verification.reason === "invalid") {
|
|
2652
3045
|
console.log("INVALID");
|
|
2653
|
-
console.error(` ! ${provider} rejected the API key (HTTP ${verification.status}).`);
|
|
3046
|
+
console.error(` ! ${triple.provider} rejected the API key (HTTP ${verification.status}).`);
|
|
2654
3047
|
console.error(
|
|
2655
3048
|
` Update it via \`npx @elisym/cli profile ${agentName}\` or set ${envVar} to a valid key.
|
|
3049
|
+
`
|
|
3050
|
+
);
|
|
3051
|
+
process.exit(1);
|
|
3052
|
+
} else if (verification.reason === "billing") {
|
|
3053
|
+
console.log("BILLING");
|
|
3054
|
+
const detail = verification.body ? ` ${verification.body.slice(0, 200)}` : "";
|
|
3055
|
+
console.error(
|
|
3056
|
+
` ! ${triple.provider} reports a billing/quota issue for ${triple.model}.${detail}`
|
|
3057
|
+
);
|
|
3058
|
+
console.error(
|
|
3059
|
+
` Top up the account at the provider console, or set ${envVar} to a key on a funded org.
|
|
2656
3060
|
`
|
|
2657
3061
|
);
|
|
2658
3062
|
process.exit(1);
|
|
2659
3063
|
} else {
|
|
2660
3064
|
console.log("unavailable");
|
|
2661
3065
|
console.warn(
|
|
2662
|
-
` ! Could not verify ${provider}
|
|
3066
|
+
` ! Could not verify ${triple.provider} ${triple.model} (${verification.error}). Continuing - jobs will fail if the key is invalid.
|
|
2663
3067
|
`
|
|
2664
3068
|
);
|
|
2665
3069
|
}
|
|
@@ -2877,28 +3281,45 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2877
3281
|
log: diagLog,
|
|
2878
3282
|
logger
|
|
2879
3283
|
});
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3284
|
+
let llmHeartbeat;
|
|
3285
|
+
if (llmSkills.length > 0) {
|
|
3286
|
+
llmHeartbeat = startLlmHeartbeat({
|
|
3287
|
+
monitor: healthMonitor,
|
|
3288
|
+
log: diagLog
|
|
3289
|
+
});
|
|
3290
|
+
diagLog("LLM health monitor armed (10min TTL, 10min heartbeat).");
|
|
3291
|
+
}
|
|
3292
|
+
const runtime = new AgentRuntime(
|
|
3293
|
+
transport,
|
|
3294
|
+
registry,
|
|
3295
|
+
skillCtx,
|
|
3296
|
+
runtimeConfig,
|
|
3297
|
+
ledger,
|
|
3298
|
+
{
|
|
3299
|
+
onJobReceived: (job) => {
|
|
3300
|
+
const cap = job.tags.find((t) => t !== "elisym") ?? "unknown";
|
|
3301
|
+
process.stdout.write(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}
|
|
2884
3302
|
`);
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
3303
|
+
logger.info({ event: "job_received", jobId: job.jobId, capability: cap });
|
|
3304
|
+
},
|
|
3305
|
+
onJobCompleted: (jobId) => {
|
|
3306
|
+
process.stdout.write(` [job] ${jobId.slice(0, 16)} | delivered
|
|
2889
3307
|
`);
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
3308
|
+
logger.info({ event: "job_delivered", jobId });
|
|
3309
|
+
},
|
|
3310
|
+
onJobError: (jobId, error) => {
|
|
3311
|
+
process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${error}
|
|
2894
3312
|
`);
|
|
2895
|
-
|
|
3313
|
+
logger.error({ event: "job_error", jobId, error });
|
|
3314
|
+
},
|
|
3315
|
+
onLog: diagLog,
|
|
3316
|
+
onStop: () => {
|
|
3317
|
+
watchdog.stop();
|
|
3318
|
+
llmHeartbeat?.stop();
|
|
3319
|
+
}
|
|
2896
3320
|
},
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
watchdog.stop();
|
|
2900
|
-
}
|
|
2901
|
-
});
|
|
3321
|
+
healthMonitor
|
|
3322
|
+
);
|
|
2902
3323
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
2903
3324
|
await runtime.run();
|
|
2904
3325
|
}
|