@elisym/cli 0.9.1 → 0.10.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 +653 -437
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -25,42 +25,542 @@ var __export = (target, all) => {
|
|
|
25
25
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
// src/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// src/llm/providers/http.ts
|
|
29
|
+
function createAbortError() {
|
|
30
|
+
const err = new Error("The operation was aborted");
|
|
31
|
+
err.name = "AbortError";
|
|
32
|
+
return err;
|
|
33
|
+
}
|
|
34
|
+
function sleepWithSignal(ms, signal) {
|
|
35
|
+
if (signal?.aborted) {
|
|
36
|
+
return Promise.reject(createAbortError());
|
|
37
|
+
}
|
|
38
|
+
if (!signal) {
|
|
39
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
40
|
+
}
|
|
41
|
+
return new Promise((resolve3, reject) => {
|
|
42
|
+
const cleanup = () => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
signal.removeEventListener("abort", onAbort);
|
|
45
|
+
};
|
|
46
|
+
const onAbort = () => {
|
|
47
|
+
cleanup();
|
|
48
|
+
reject(createAbortError());
|
|
49
|
+
};
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
cleanup();
|
|
52
|
+
resolve3();
|
|
53
|
+
}, ms);
|
|
54
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function fetchWithTimeout(url, init, signal) {
|
|
58
|
+
if (signal?.aborted) {
|
|
59
|
+
throw createAbortError();
|
|
60
|
+
}
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
|
|
63
|
+
const onAbort = () => controller.abort();
|
|
64
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
65
|
+
try {
|
|
66
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
signal?.removeEventListener("abort", onAbort);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function fetchWithRetry(url, init, signal) {
|
|
73
|
+
for (let attempt = 0; ; attempt++) {
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await fetchWithTimeout(url, init, signal);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const name = error instanceof Error ? error.name : "";
|
|
79
|
+
if (attempt >= MAX_RETRIES || name === "AbortError") {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
await sleepWithSignal(Math.min(1e3 * 2 ** attempt, 8e3), signal);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (response.ok || attempt >= MAX_RETRIES || !RETRYABLE_STATUSES.has(response.status)) {
|
|
86
|
+
return response;
|
|
87
|
+
}
|
|
88
|
+
const retryAfter = response.headers.get("retry-after");
|
|
89
|
+
const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3 || 1e3 * 2 ** attempt, 3e4) : Math.min(1e3 * 2 ** attempt, 8e3);
|
|
90
|
+
await response.body?.cancel().catch(() => void 0);
|
|
91
|
+
await sleepWithSignal(delay, signal);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
var LLM_TIMEOUT_MS, MAX_RETRIES, RETRYABLE_STATUSES;
|
|
95
|
+
var init_http = __esm({
|
|
96
|
+
"src/llm/providers/http.ts"() {
|
|
97
|
+
LLM_TIMEOUT_MS = 12e4;
|
|
98
|
+
MAX_RETRIES = 2;
|
|
99
|
+
RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
100
|
+
}
|
|
33
101
|
});
|
|
34
|
-
|
|
102
|
+
|
|
103
|
+
// src/llm/providers/anthropic.ts
|
|
104
|
+
async function fetchModels(apiKey, signal) {
|
|
35
105
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
106
|
+
const response = await fetchWithTimeout(
|
|
107
|
+
"https://api.anthropic.com/v1/models?limit=1000",
|
|
108
|
+
{
|
|
109
|
+
method: "GET",
|
|
38
110
|
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;
|
|
111
|
+
},
|
|
112
|
+
signal
|
|
113
|
+
);
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
return FALLBACK_MODELS;
|
|
46
116
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
117
|
+
const data = await response.json();
|
|
118
|
+
const models = (data.data ?? []).map((entry) => entry.id).sort();
|
|
119
|
+
return models.length > 0 ? models : FALLBACK_MODELS;
|
|
120
|
+
} catch {
|
|
121
|
+
return FALLBACK_MODELS;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function verifyKey(apiKey, signal) {
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetchWithTimeout(
|
|
127
|
+
"https://api.anthropic.com/v1/models?limit=1",
|
|
128
|
+
{
|
|
129
|
+
method: "GET",
|
|
130
|
+
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
|
|
131
|
+
},
|
|
132
|
+
signal
|
|
133
|
+
);
|
|
134
|
+
if (response.ok) {
|
|
135
|
+
await response.body?.cancel().catch(() => void 0);
|
|
136
|
+
return { ok: true };
|
|
137
|
+
}
|
|
138
|
+
const body = (await response.text().catch(() => "")).slice(0, 500);
|
|
139
|
+
if (response.status === 401 || response.status === 403) {
|
|
140
|
+
return { ok: false, reason: "invalid", status: response.status, body };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
reason: "unavailable",
|
|
145
|
+
error: `HTTP ${response.status}: ${body.slice(0, 200)}`
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
149
|
+
return { ok: false, reason: "unavailable", error: message };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
var DEFAULT_MODEL, DEFAULT_MAX_TOKENS, FALLBACK_MODELS, AnthropicClient, ANTHROPIC_PROVIDER;
|
|
153
|
+
var init_anthropic = __esm({
|
|
154
|
+
"src/llm/providers/anthropic.ts"() {
|
|
155
|
+
init_http();
|
|
156
|
+
DEFAULT_MODEL = "claude-haiku-4-5-20251001";
|
|
157
|
+
DEFAULT_MAX_TOKENS = 4096;
|
|
158
|
+
FALLBACK_MODELS = ["claude-sonnet-4-6", "claude-haiku-4-5-20251001", "claude-opus-4-6"];
|
|
159
|
+
AnthropicClient = class {
|
|
160
|
+
constructor(config) {
|
|
161
|
+
this.config = config;
|
|
162
|
+
}
|
|
163
|
+
totalIn = 0;
|
|
164
|
+
totalOut = 0;
|
|
165
|
+
logTokens(usage) {
|
|
166
|
+
if (!this.config.logUsage || !usage) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const inputTokens = usage.input_tokens ?? 0;
|
|
170
|
+
const outputTokens = usage.output_tokens ?? 0;
|
|
171
|
+
this.totalIn += inputTokens;
|
|
172
|
+
this.totalOut += outputTokens;
|
|
173
|
+
console.log(
|
|
174
|
+
` [LLM] ${this.config.model} tokens: in=${inputTokens} out=${outputTokens} (total: in=${this.totalIn} out=${this.totalOut})`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
async complete(systemPrompt, userInput, signal) {
|
|
178
|
+
const response = await fetchWithRetry(
|
|
179
|
+
"https://api.anthropic.com/v1/messages",
|
|
180
|
+
{
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: {
|
|
183
|
+
"Content-Type": "application/json",
|
|
184
|
+
"x-api-key": this.config.apiKey,
|
|
185
|
+
"anthropic-version": "2023-06-01"
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
model: this.config.model,
|
|
189
|
+
max_tokens: this.config.maxTokens,
|
|
190
|
+
system: systemPrompt,
|
|
191
|
+
messages: [{ role: "user", content: userInput }]
|
|
192
|
+
})
|
|
193
|
+
},
|
|
194
|
+
signal
|
|
195
|
+
);
|
|
196
|
+
if (!response.ok) {
|
|
197
|
+
throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
|
|
198
|
+
}
|
|
199
|
+
const data = await response.json();
|
|
200
|
+
this.logTokens(data.usage);
|
|
201
|
+
const textBlock = data.content?.find((block) => block.type === "text");
|
|
202
|
+
return textBlock?.text ?? "";
|
|
203
|
+
}
|
|
204
|
+
async completeWithTools(systemPrompt, messages, tools, signal) {
|
|
205
|
+
const anthropicTools = tools.map((tool) => ({
|
|
206
|
+
name: tool.name,
|
|
207
|
+
description: tool.description,
|
|
208
|
+
input_schema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: Object.fromEntries(
|
|
211
|
+
tool.parameters.map((param) => [
|
|
212
|
+
param.name,
|
|
213
|
+
{ type: "string", description: param.description }
|
|
214
|
+
])
|
|
215
|
+
),
|
|
216
|
+
required: tool.parameters.filter((param) => param.required).map((param) => param.name)
|
|
217
|
+
}
|
|
218
|
+
}));
|
|
219
|
+
const response = await fetchWithRetry(
|
|
220
|
+
"https://api.anthropic.com/v1/messages",
|
|
221
|
+
{
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: {
|
|
224
|
+
"Content-Type": "application/json",
|
|
225
|
+
"x-api-key": this.config.apiKey,
|
|
226
|
+
"anthropic-version": "2023-06-01"
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
model: this.config.model,
|
|
230
|
+
max_tokens: this.config.maxTokens,
|
|
231
|
+
system: systemPrompt,
|
|
232
|
+
messages,
|
|
233
|
+
tools: anthropicTools
|
|
234
|
+
})
|
|
235
|
+
},
|
|
236
|
+
signal
|
|
237
|
+
);
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
|
|
240
|
+
}
|
|
241
|
+
const data = await response.json();
|
|
242
|
+
this.logTokens(data.usage);
|
|
243
|
+
const content = data.content ?? [];
|
|
244
|
+
const toolUses = content.filter((block) => block.type === "tool_use");
|
|
245
|
+
if (toolUses.length > 0) {
|
|
246
|
+
const calls = toolUses.map((block) => ({
|
|
247
|
+
id: block.id ?? "",
|
|
248
|
+
name: block.name ?? "",
|
|
249
|
+
arguments: block.input ?? {}
|
|
250
|
+
}));
|
|
251
|
+
return {
|
|
252
|
+
type: "tool_use",
|
|
253
|
+
calls,
|
|
254
|
+
assistantMessage: { role: "assistant", content }
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const textBlock = content.find((block) => block.type === "text");
|
|
258
|
+
return { type: "text", text: textBlock?.text ?? "" };
|
|
53
259
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
260
|
+
formatToolResultMessages(results) {
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
role: "user",
|
|
264
|
+
content: results.map((result) => ({
|
|
265
|
+
type: "tool_result",
|
|
266
|
+
tool_use_id: result.callId,
|
|
267
|
+
content: result.content
|
|
268
|
+
}))
|
|
269
|
+
}
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
ANTHROPIC_PROVIDER = {
|
|
274
|
+
id: "anthropic",
|
|
275
|
+
displayName: "Anthropic (Claude)",
|
|
276
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
277
|
+
defaultModel: DEFAULT_MODEL,
|
|
278
|
+
fallbackModels: FALLBACK_MODELS,
|
|
279
|
+
fetchModels,
|
|
280
|
+
verifyKey,
|
|
281
|
+
createClient: (config) => new AnthropicClient({
|
|
282
|
+
apiKey: config.apiKey,
|
|
283
|
+
model: config.model ?? DEFAULT_MODEL,
|
|
284
|
+
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
285
|
+
logUsage: config.logUsage
|
|
286
|
+
})
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// src/llm/providers/openai.ts
|
|
292
|
+
function isOpenAIReasoningModel(model) {
|
|
293
|
+
return /^o\d/.test(model) || /^gpt-5(\b|[-.])/.test(model);
|
|
294
|
+
}
|
|
295
|
+
async function fetchModels2(apiKey, signal) {
|
|
296
|
+
try {
|
|
297
|
+
const response = await fetchWithTimeout(
|
|
298
|
+
"https://api.openai.com/v1/models",
|
|
299
|
+
{
|
|
300
|
+
method: "GET",
|
|
301
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
302
|
+
},
|
|
303
|
+
signal
|
|
304
|
+
);
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
return FALLBACK_MODELS2;
|
|
307
|
+
}
|
|
308
|
+
const data = await response.json();
|
|
309
|
+
const models = (data.data ?? []).map((entry) => entry.id).filter(
|
|
310
|
+
(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
|
+
).sort();
|
|
312
|
+
return models.length > 0 ? models : FALLBACK_MODELS2;
|
|
313
|
+
} catch {
|
|
314
|
+
return FALLBACK_MODELS2;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function verifyKey2(apiKey, signal) {
|
|
318
|
+
try {
|
|
319
|
+
const response = await fetchWithTimeout(
|
|
320
|
+
"https://api.openai.com/v1/models",
|
|
321
|
+
{
|
|
322
|
+
method: "GET",
|
|
323
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
324
|
+
},
|
|
325
|
+
signal
|
|
326
|
+
);
|
|
327
|
+
if (response.ok) {
|
|
328
|
+
await response.body?.cancel().catch(() => void 0);
|
|
329
|
+
return { ok: true };
|
|
330
|
+
}
|
|
331
|
+
const body = (await response.text().catch(() => "")).slice(0, 500);
|
|
332
|
+
if (response.status === 401 || response.status === 403) {
|
|
333
|
+
return { ok: false, reason: "invalid", status: response.status, body };
|
|
59
334
|
}
|
|
60
|
-
return
|
|
335
|
+
return {
|
|
336
|
+
ok: false,
|
|
337
|
+
reason: "unavailable",
|
|
338
|
+
error: `HTTP ${response.status}: ${body.slice(0, 200)}`
|
|
339
|
+
};
|
|
340
|
+
} catch (error) {
|
|
341
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
342
|
+
return { ok: false, reason: "unavailable", error: message };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
var DEFAULT_MODEL2, DEFAULT_MAX_TOKENS2, FALLBACK_MODELS2, OpenAIClient, OPENAI_PROVIDER;
|
|
346
|
+
var init_openai = __esm({
|
|
347
|
+
"src/llm/providers/openai.ts"() {
|
|
348
|
+
init_http();
|
|
349
|
+
DEFAULT_MODEL2 = "gpt-4o-mini";
|
|
350
|
+
DEFAULT_MAX_TOKENS2 = 4096;
|
|
351
|
+
FALLBACK_MODELS2 = ["gpt-4o", "gpt-4o-mini", "o3-mini"];
|
|
352
|
+
OpenAIClient = class {
|
|
353
|
+
constructor(config) {
|
|
354
|
+
this.config = config;
|
|
355
|
+
}
|
|
356
|
+
totalIn = 0;
|
|
357
|
+
totalOut = 0;
|
|
358
|
+
isReasoningModel() {
|
|
359
|
+
return isOpenAIReasoningModel(this.config.model);
|
|
360
|
+
}
|
|
361
|
+
logTokens(usage) {
|
|
362
|
+
if (!this.config.logUsage || !usage) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const inputTokens = usage.prompt_tokens ?? 0;
|
|
366
|
+
const outputTokens = usage.completion_tokens ?? 0;
|
|
367
|
+
this.totalIn += inputTokens;
|
|
368
|
+
this.totalOut += outputTokens;
|
|
369
|
+
console.log(
|
|
370
|
+
` [LLM] ${this.config.model} tokens: in=${inputTokens} out=${outputTokens} (total: in=${this.totalIn} out=${this.totalOut})`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
async complete(systemPrompt, userInput, signal) {
|
|
374
|
+
const reasoning = this.isReasoningModel();
|
|
375
|
+
const response = await fetchWithRetry(
|
|
376
|
+
"https://api.openai.com/v1/chat/completions",
|
|
377
|
+
{
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: {
|
|
380
|
+
"Content-Type": "application/json",
|
|
381
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
382
|
+
},
|
|
383
|
+
body: JSON.stringify({
|
|
384
|
+
model: this.config.model,
|
|
385
|
+
...reasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
|
|
386
|
+
messages: [
|
|
387
|
+
{ role: reasoning ? "developer" : "system", content: systemPrompt },
|
|
388
|
+
{ role: "user", content: userInput }
|
|
389
|
+
]
|
|
390
|
+
})
|
|
391
|
+
},
|
|
392
|
+
signal
|
|
393
|
+
);
|
|
394
|
+
if (!response.ok) {
|
|
395
|
+
throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
|
|
396
|
+
}
|
|
397
|
+
const data = await response.json();
|
|
398
|
+
this.logTokens(data.usage);
|
|
399
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
400
|
+
}
|
|
401
|
+
async completeWithTools(systemPrompt, messages, tools, signal) {
|
|
402
|
+
const openaiTools = tools.map((tool) => ({
|
|
403
|
+
type: "function",
|
|
404
|
+
function: {
|
|
405
|
+
name: tool.name,
|
|
406
|
+
description: tool.description,
|
|
407
|
+
parameters: {
|
|
408
|
+
type: "object",
|
|
409
|
+
properties: Object.fromEntries(
|
|
410
|
+
tool.parameters.map((param) => [
|
|
411
|
+
param.name,
|
|
412
|
+
{ type: "string", description: param.description }
|
|
413
|
+
])
|
|
414
|
+
),
|
|
415
|
+
required: tool.parameters.filter((param) => param.required).map((param) => param.name)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}));
|
|
419
|
+
const reasoning = this.isReasoningModel();
|
|
420
|
+
const response = await fetchWithRetry(
|
|
421
|
+
"https://api.openai.com/v1/chat/completions",
|
|
422
|
+
{
|
|
423
|
+
method: "POST",
|
|
424
|
+
headers: {
|
|
425
|
+
"Content-Type": "application/json",
|
|
426
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
427
|
+
},
|
|
428
|
+
body: JSON.stringify({
|
|
429
|
+
model: this.config.model,
|
|
430
|
+
...reasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
|
|
431
|
+
messages: [
|
|
432
|
+
{ role: reasoning ? "developer" : "system", content: systemPrompt },
|
|
433
|
+
...messages
|
|
434
|
+
],
|
|
435
|
+
tools: openaiTools
|
|
436
|
+
})
|
|
437
|
+
},
|
|
438
|
+
signal
|
|
439
|
+
);
|
|
440
|
+
if (!response.ok) {
|
|
441
|
+
throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
|
|
442
|
+
}
|
|
443
|
+
const data = await response.json();
|
|
444
|
+
this.logTokens(data.usage);
|
|
445
|
+
const message = data.choices?.[0]?.message;
|
|
446
|
+
const toolCalls = message?.tool_calls ?? [];
|
|
447
|
+
if (toolCalls.length > 0) {
|
|
448
|
+
const calls = toolCalls.map((call) => {
|
|
449
|
+
let args;
|
|
450
|
+
try {
|
|
451
|
+
args = JSON.parse(call.function?.arguments ?? "{}");
|
|
452
|
+
} catch {
|
|
453
|
+
args = {};
|
|
454
|
+
}
|
|
455
|
+
return { id: call.id ?? "", name: call.function?.name ?? "", arguments: args };
|
|
456
|
+
});
|
|
457
|
+
return { type: "tool_use", calls, assistantMessage: message };
|
|
458
|
+
}
|
|
459
|
+
return { type: "text", text: message?.content ?? "" };
|
|
460
|
+
}
|
|
461
|
+
formatToolResultMessages(results) {
|
|
462
|
+
return results.map((result) => ({
|
|
463
|
+
role: "tool",
|
|
464
|
+
tool_call_id: result.callId,
|
|
465
|
+
content: result.content
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
OPENAI_PROVIDER = {
|
|
470
|
+
id: "openai",
|
|
471
|
+
displayName: "OpenAI (GPT)",
|
|
472
|
+
envVar: "OPENAI_API_KEY",
|
|
473
|
+
defaultModel: DEFAULT_MODEL2,
|
|
474
|
+
fallbackModels: FALLBACK_MODELS2,
|
|
475
|
+
fetchModels: fetchModels2,
|
|
476
|
+
verifyKey: verifyKey2,
|
|
477
|
+
createClient: (config) => new OpenAIClient({
|
|
478
|
+
apiKey: config.apiKey,
|
|
479
|
+
model: config.model ?? DEFAULT_MODEL2,
|
|
480
|
+
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS2,
|
|
481
|
+
logUsage: config.logUsage
|
|
482
|
+
}),
|
|
483
|
+
isReasoningModel: isOpenAIReasoningModel
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// src/llm/registry.ts
|
|
489
|
+
function registerLlmProvider(descriptor) {
|
|
490
|
+
REGISTRY.set(descriptor.id, descriptor);
|
|
491
|
+
}
|
|
492
|
+
function getLlmProvider(id) {
|
|
493
|
+
return REGISTRY.get(id);
|
|
494
|
+
}
|
|
495
|
+
function listLlmProviders() {
|
|
496
|
+
return Array.from(REGISTRY.values());
|
|
497
|
+
}
|
|
498
|
+
function getRegisteredProviderIds() {
|
|
499
|
+
return Array.from(REGISTRY.keys());
|
|
500
|
+
}
|
|
501
|
+
var REGISTRY;
|
|
502
|
+
var init_registry = __esm({
|
|
503
|
+
"src/llm/registry.ts"() {
|
|
504
|
+
init_anthropic();
|
|
505
|
+
init_openai();
|
|
506
|
+
REGISTRY = /* @__PURE__ */ new Map();
|
|
507
|
+
registerLlmProvider(ANTHROPIC_PROVIDER);
|
|
508
|
+
registerLlmProvider(OPENAI_PROVIDER);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// src/llm/index.ts
|
|
513
|
+
function createLlmClient(config) {
|
|
514
|
+
const descriptor = getLlmProvider(config.provider);
|
|
515
|
+
if (!descriptor) {
|
|
516
|
+
const known = getRegisteredProviderIds().join(", ") || "<none>";
|
|
517
|
+
throw new Error(`Unknown LLM provider "${config.provider}". Registered: ${known}.`);
|
|
518
|
+
}
|
|
519
|
+
if (!config.apiKey) {
|
|
520
|
+
throw new Error(`${descriptor.envVar} is required for skill runtime`);
|
|
521
|
+
}
|
|
522
|
+
return descriptor.createClient({
|
|
523
|
+
apiKey: config.apiKey,
|
|
524
|
+
model: config.model,
|
|
525
|
+
maxTokens: config.maxTokens,
|
|
526
|
+
logUsage: config.logUsage
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
async function verifyLlmApiKey(provider, apiKey, signal) {
|
|
530
|
+
const descriptor = getLlmProvider(provider);
|
|
531
|
+
if (!descriptor) {
|
|
532
|
+
return {
|
|
533
|
+
ok: false,
|
|
534
|
+
reason: "unavailable",
|
|
535
|
+
error: `Unknown LLM provider "${provider}"`
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return descriptor.verifyKey(apiKey, signal);
|
|
539
|
+
}
|
|
540
|
+
var init_llm = __esm({
|
|
541
|
+
"src/llm/index.ts"() {
|
|
542
|
+
init_registry();
|
|
543
|
+
init_registry();
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// src/commands/init.ts
|
|
548
|
+
var init_exports = {};
|
|
549
|
+
__export(init_exports, {
|
|
550
|
+
cmdInit: () => cmdInit,
|
|
551
|
+
fetchModels: () => fetchModels3
|
|
552
|
+
});
|
|
553
|
+
async function fetchModels3(provider, apiKey) {
|
|
554
|
+
const descriptor = getLlmProvider(provider);
|
|
555
|
+
if (!descriptor) {
|
|
556
|
+
console.warn(` ! Unknown provider "${provider}". No models available.`);
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
return await descriptor.fetchModels(apiKey);
|
|
61
561
|
} catch (e) {
|
|
62
562
|
console.warn(` ! Could not fetch models: ${e.message}. Using defaults.`);
|
|
63
|
-
return
|
|
563
|
+
return descriptor.fallbackModels;
|
|
64
564
|
}
|
|
65
565
|
}
|
|
66
566
|
function pickTarget(options) {
|
|
@@ -140,55 +640,61 @@ async function cmdInit(nameArg, options = {}) {
|
|
|
140
640
|
promptedApiKey = result.apiKey;
|
|
141
641
|
}
|
|
142
642
|
let defaultProviderKey;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
643
|
+
const defaultProviderId = yaml.llm?.provider;
|
|
644
|
+
if (yaml.llm && defaultProviderId) {
|
|
645
|
+
const descriptor = getLlmProvider(defaultProviderId);
|
|
646
|
+
const envKey = descriptor ? process.env[descriptor.envVar] : void 0;
|
|
647
|
+
if (envKey && descriptor) {
|
|
146
648
|
defaultProviderKey = envKey;
|
|
147
|
-
console.log(
|
|
148
|
-
` Using ${yaml.llm.provider === "anthropic" ? "ANTHROPIC" : "OPENAI"}_API_KEY from environment.`
|
|
149
|
-
);
|
|
649
|
+
console.log(` Using ${descriptor.envVar} from environment.`);
|
|
150
650
|
} else if (promptedApiKey) {
|
|
151
651
|
defaultProviderKey = promptedApiKey;
|
|
152
652
|
} else {
|
|
653
|
+
const label = descriptor?.displayName ?? defaultProviderId;
|
|
153
654
|
const { apiKey } = await inquirer.prompt([
|
|
154
655
|
{
|
|
155
656
|
type: "password",
|
|
156
657
|
name: "apiKey",
|
|
157
|
-
message: `${
|
|
658
|
+
message: `${label} API key:`,
|
|
158
659
|
mask: "*"
|
|
159
660
|
}
|
|
160
661
|
]);
|
|
161
662
|
defaultProviderKey = apiKey || void 0;
|
|
162
663
|
}
|
|
163
664
|
}
|
|
164
|
-
|
|
165
|
-
if (yaml.llm && !template) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
665
|
+
const otherProviderKeys = /* @__PURE__ */ new Map();
|
|
666
|
+
if (yaml.llm && !template && defaultProviderId) {
|
|
667
|
+
const otherDescriptors = listLlmProviders().filter(
|
|
668
|
+
(descriptor) => descriptor.id !== defaultProviderId
|
|
669
|
+
);
|
|
670
|
+
for (const descriptor of otherDescriptors) {
|
|
671
|
+
const otherEnvKey = process.env[descriptor.envVar];
|
|
672
|
+
const { configureOther } = await inquirer.prompt([
|
|
673
|
+
{
|
|
674
|
+
type: "confirm",
|
|
675
|
+
name: "configureOther",
|
|
676
|
+
message: `Configure ${descriptor.displayName} API key too (for skills that override the default provider)?`,
|
|
677
|
+
default: Boolean(otherEnvKey)
|
|
678
|
+
}
|
|
679
|
+
]);
|
|
680
|
+
if (!configureOther) {
|
|
681
|
+
continue;
|
|
176
682
|
}
|
|
177
|
-
]);
|
|
178
|
-
if (configureOther) {
|
|
179
683
|
if (otherEnvKey) {
|
|
180
|
-
|
|
181
|
-
console.log(` Using ${
|
|
684
|
+
otherProviderKeys.set(descriptor.id, otherEnvKey);
|
|
685
|
+
console.log(` Using ${descriptor.envVar} from environment.`);
|
|
182
686
|
} else {
|
|
183
687
|
const { promptedOther } = await inquirer.prompt([
|
|
184
688
|
{
|
|
185
689
|
type: "password",
|
|
186
690
|
name: "promptedOther",
|
|
187
|
-
message: `${
|
|
691
|
+
message: `${descriptor.displayName} API key:`,
|
|
188
692
|
mask: "*"
|
|
189
693
|
}
|
|
190
694
|
]);
|
|
191
|
-
|
|
695
|
+
if (promptedOther) {
|
|
696
|
+
otherProviderKeys.set(descriptor.id, promptedOther);
|
|
697
|
+
}
|
|
192
698
|
}
|
|
193
699
|
}
|
|
194
700
|
}
|
|
@@ -224,15 +730,17 @@ async function cmdInit(nameArg, options = {}) {
|
|
|
224
730
|
const nostrPubkey = getPublicKey(nostrSecretBytes);
|
|
225
731
|
const nostrSecretHex = Buffer.from(nostrSecretBytes).toString("hex");
|
|
226
732
|
const created = await createAgentDir({ target, name: agentName, cwd });
|
|
227
|
-
const
|
|
228
|
-
|
|
733
|
+
const collectedKeys = new Map(otherProviderKeys);
|
|
734
|
+
if (yaml.llm && defaultProviderKey) {
|
|
735
|
+
collectedKeys.set(yaml.llm.provider, defaultProviderKey);
|
|
736
|
+
}
|
|
737
|
+
const llmApiKeys = Object.fromEntries(collectedKeys);
|
|
229
738
|
await writeYaml(created.dir, yaml);
|
|
230
739
|
await writeSecrets(
|
|
231
740
|
created.dir,
|
|
232
741
|
{
|
|
233
742
|
nostr_secret_key: nostrSecretHex,
|
|
234
|
-
|
|
235
|
-
openai_api_key: openaiKey
|
|
743
|
+
llm_api_keys: Object.keys(llmApiKeys).length > 0 ? llmApiKeys : void 0
|
|
236
744
|
},
|
|
237
745
|
passphrase || void 0
|
|
238
746
|
);
|
|
@@ -334,8 +842,10 @@ async function promptYaml(inquirer) {
|
|
|
334
842
|
name: "None (non-LLM agent - static-file / static-script / dynamic-script only)",
|
|
335
843
|
value: "none"
|
|
336
844
|
},
|
|
337
|
-
|
|
338
|
-
|
|
845
|
+
...listLlmProviders().map((descriptor2) => ({
|
|
846
|
+
name: descriptor2.displayName,
|
|
847
|
+
value: descriptor2.id
|
|
848
|
+
}))
|
|
339
849
|
]
|
|
340
850
|
}
|
|
341
851
|
]);
|
|
@@ -352,21 +862,25 @@ async function promptYaml(inquirer) {
|
|
|
352
862
|
const yaml2 = ElisymYamlSchema.parse(baseYaml);
|
|
353
863
|
return { yaml: yaml2 };
|
|
354
864
|
}
|
|
355
|
-
const
|
|
865
|
+
const descriptor = getLlmProvider(llmProvider);
|
|
866
|
+
if (!descriptor) {
|
|
867
|
+
throw new Error(`Internal: provider "${llmProvider}" not registered.`);
|
|
868
|
+
}
|
|
869
|
+
const envKey = process.env[descriptor.envVar];
|
|
356
870
|
let apiKey = envKey;
|
|
357
871
|
if (!apiKey) {
|
|
358
872
|
const { promptedKey } = await inquirer.prompt([
|
|
359
873
|
{
|
|
360
874
|
type: "password",
|
|
361
875
|
name: "promptedKey",
|
|
362
|
-
message: `${
|
|
876
|
+
message: `${descriptor.displayName} API key:`,
|
|
363
877
|
mask: "*"
|
|
364
878
|
}
|
|
365
879
|
]);
|
|
366
880
|
apiKey = promptedKey || void 0;
|
|
367
881
|
}
|
|
368
882
|
console.log(" Fetching available models...");
|
|
369
|
-
const models = await
|
|
883
|
+
const models = await fetchModels3(llmProvider, apiKey ?? "");
|
|
370
884
|
const { model } = await inquirer.prompt([
|
|
371
885
|
{
|
|
372
886
|
type: "list",
|
|
@@ -389,18 +903,17 @@ async function promptYaml(inquirer) {
|
|
|
389
903
|
});
|
|
390
904
|
return { yaml, apiKey: envKey ? void 0 : apiKey };
|
|
391
905
|
}
|
|
392
|
-
var FALLBACK_MODELS;
|
|
393
906
|
var init_init = __esm({
|
|
394
907
|
"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
|
-
};
|
|
908
|
+
init_llm();
|
|
399
909
|
}
|
|
400
910
|
});
|
|
401
911
|
|
|
402
912
|
// src/index.ts
|
|
403
913
|
init_init();
|
|
914
|
+
|
|
915
|
+
// src/commands/profile.ts
|
|
916
|
+
init_llm();
|
|
404
917
|
async function cmdProfile(name) {
|
|
405
918
|
const { default: inquirer } = await import('inquirer');
|
|
406
919
|
const cwd = process.cwd();
|
|
@@ -519,32 +1032,45 @@ async function cmdProfile(name) {
|
|
|
519
1032
|
console.log(" Wallet updated.\n");
|
|
520
1033
|
}
|
|
521
1034
|
if (section === "llm") {
|
|
1035
|
+
const providerChoices = listLlmProviders().map((descriptor) => ({
|
|
1036
|
+
name: descriptor.displayName,
|
|
1037
|
+
value: descriptor.id
|
|
1038
|
+
}));
|
|
1039
|
+
if (providerChoices.length === 0) {
|
|
1040
|
+
console.error(" ! No LLM providers registered.");
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const firstChoice = providerChoices[0];
|
|
1044
|
+
if (!firstChoice) {
|
|
1045
|
+
console.error(" ! No LLM providers registered.");
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
522
1048
|
const { llmProvider } = await inquirer.prompt([
|
|
523
1049
|
{
|
|
524
1050
|
type: "list",
|
|
525
1051
|
name: "llmProvider",
|
|
526
1052
|
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"
|
|
1053
|
+
choices: providerChoices,
|
|
1054
|
+
default: loaded.yaml.llm?.provider ?? firstChoice.value
|
|
532
1055
|
}
|
|
533
1056
|
]);
|
|
534
|
-
const
|
|
1057
|
+
const defaultDescriptor = getLlmProvider(llmProvider);
|
|
1058
|
+
if (!defaultDescriptor) {
|
|
1059
|
+
throw new Error(`Internal: provider "${llmProvider}" not registered.`);
|
|
1060
|
+
}
|
|
535
1061
|
const defaultKeyStatus = describeKeyStatus(loaded.secrets, llmProvider);
|
|
536
1062
|
const { apiKey } = await inquirer.prompt([
|
|
537
1063
|
{
|
|
538
1064
|
type: "password",
|
|
539
1065
|
name: "apiKey",
|
|
540
|
-
message: `${
|
|
1066
|
+
message: `${defaultDescriptor.displayName} API key [${defaultKeyStatus}] (leave empty to keep current):`,
|
|
541
1067
|
mask: "*"
|
|
542
1068
|
}
|
|
543
1069
|
]);
|
|
544
1070
|
const probeKey = apiKey || pickPlainKey(loaded.secrets, llmProvider);
|
|
545
1071
|
console.log(" Fetching available models...");
|
|
546
|
-
const { fetchModels:
|
|
547
|
-
const models = await
|
|
1072
|
+
const { fetchModels: fetchModels4 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1073
|
+
const models = await fetchModels4(llmProvider, probeKey);
|
|
548
1074
|
const { model } = await inquirer.prompt([
|
|
549
1075
|
{
|
|
550
1076
|
type: "list",
|
|
@@ -562,35 +1088,42 @@ async function cmdProfile(name) {
|
|
|
562
1088
|
default: loaded.yaml.llm?.max_tokens ?? 4096
|
|
563
1089
|
}
|
|
564
1090
|
]);
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1091
|
+
const otherDescriptors = listLlmProviders().filter(
|
|
1092
|
+
(descriptor) => descriptor.id !== llmProvider
|
|
1093
|
+
);
|
|
1094
|
+
const otherKeys = /* @__PURE__ */ new Map();
|
|
1095
|
+
for (const descriptor of otherDescriptors) {
|
|
1096
|
+
const status = describeKeyStatus(loaded.secrets, descriptor.id);
|
|
1097
|
+
const { otherApiKey } = await inquirer.prompt([
|
|
1098
|
+
{
|
|
1099
|
+
type: "password",
|
|
1100
|
+
name: "otherApiKey",
|
|
1101
|
+
message: `${descriptor.displayName} API key for skill overrides [${status}] (leave empty to keep current):`,
|
|
1102
|
+
mask: "*"
|
|
1103
|
+
}
|
|
1104
|
+
]);
|
|
1105
|
+
if (otherApiKey) {
|
|
1106
|
+
otherKeys.set(descriptor.id, otherApiKey);
|
|
572
1107
|
}
|
|
573
|
-
|
|
1108
|
+
}
|
|
574
1109
|
const nextLlm = { provider: llmProvider, model, max_tokens: maxTokens };
|
|
575
1110
|
const nextYaml = { ...loaded.yaml, llm: nextLlm };
|
|
576
1111
|
await writeYaml(loaded.dir, nextYaml);
|
|
577
1112
|
loaded.yaml = nextYaml;
|
|
578
|
-
if (apiKey ||
|
|
579
|
-
const
|
|
1113
|
+
if (apiKey || otherKeys.size > 0) {
|
|
1114
|
+
const nextLlmApiKeys = {
|
|
1115
|
+
...loaded.secrets.llm_api_keys ?? {}
|
|
1116
|
+
};
|
|
580
1117
|
if (apiKey) {
|
|
581
|
-
|
|
582
|
-
nextSecrets.anthropic_api_key = apiKey;
|
|
583
|
-
} else {
|
|
584
|
-
nextSecrets.openai_api_key = apiKey;
|
|
585
|
-
}
|
|
1118
|
+
nextLlmApiKeys[llmProvider] = apiKey;
|
|
586
1119
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
nextSecrets.anthropic_api_key = otherApiKey;
|
|
590
|
-
} else {
|
|
591
|
-
nextSecrets.openai_api_key = otherApiKey;
|
|
592
|
-
}
|
|
1120
|
+
for (const [providerId, key] of otherKeys) {
|
|
1121
|
+
nextLlmApiKeys[providerId] = key;
|
|
593
1122
|
}
|
|
1123
|
+
const nextSecrets = {
|
|
1124
|
+
...loaded.secrets,
|
|
1125
|
+
llm_api_keys: nextLlmApiKeys
|
|
1126
|
+
};
|
|
594
1127
|
await writeSecrets(loaded.dir, nextSecrets, passphrase);
|
|
595
1128
|
loaded.secrets = nextSecrets;
|
|
596
1129
|
}
|
|
@@ -606,16 +1139,12 @@ function truncate(value, max = 40) {
|
|
|
606
1139
|
}
|
|
607
1140
|
return value.slice(0, max - 1) + "\u2026";
|
|
608
1141
|
}
|
|
609
|
-
function
|
|
610
|
-
return
|
|
1142
|
+
function describeKeyStatus(secrets, providerId) {
|
|
1143
|
+
return secrets.llm_api_keys?.[providerId] ? "set" : "not set";
|
|
611
1144
|
}
|
|
612
|
-
function
|
|
613
|
-
const
|
|
614
|
-
return
|
|
615
|
-
}
|
|
616
|
-
function pickPlainKey(secrets, provider) {
|
|
617
|
-
const perProviderField = provider === "anthropic" ? "anthropic_api_key" : "openai_api_key";
|
|
618
|
-
return secrets[perProviderField] ?? "";
|
|
1145
|
+
function pickPlainKey(secrets, providerId) {
|
|
1146
|
+
const value = secrets.llm_api_keys?.[providerId];
|
|
1147
|
+
return typeof value === "string" ? value : "";
|
|
619
1148
|
}
|
|
620
1149
|
var TCP_PROBE_TIMEOUT_MS = 3e3;
|
|
621
1150
|
function parseRelayUrl(url) {
|
|
@@ -875,322 +1404,11 @@ var JobLedger = class {
|
|
|
875
1404
|
}
|
|
876
1405
|
};
|
|
877
1406
|
|
|
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
|
-
};
|
|
1407
|
+
// src/commands/start.ts
|
|
1408
|
+
init_llm();
|
|
1191
1409
|
|
|
1192
1410
|
// src/llm/resolve.ts
|
|
1193
|
-
var
|
|
1411
|
+
var DEFAULT_MAX_TOKENS3 = 4096;
|
|
1194
1412
|
function resolveSkillLlm(input, agentDefault) {
|
|
1195
1413
|
const override = input.llmOverride;
|
|
1196
1414
|
const overridePairSet = override?.provider !== void 0 && override.model !== void 0;
|
|
@@ -1208,7 +1426,7 @@ function resolveSkillLlm(input, agentDefault) {
|
|
|
1208
1426
|
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
1427
|
};
|
|
1210
1428
|
}
|
|
1211
|
-
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ??
|
|
1429
|
+
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS3;
|
|
1212
1430
|
return { provider, model, maxTokens };
|
|
1213
1431
|
}
|
|
1214
1432
|
|
|
@@ -1234,34 +1452,32 @@ function resolveTripleForOverride(override, agentDefault) {
|
|
|
1234
1452
|
if (!provider || !model) {
|
|
1235
1453
|
return void 0;
|
|
1236
1454
|
}
|
|
1237
|
-
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ??
|
|
1455
|
+
const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS3;
|
|
1238
1456
|
return { provider, model, maxTokens };
|
|
1239
1457
|
}
|
|
1240
1458
|
|
|
1241
1459
|
// 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
|
-
};
|
|
1460
|
+
init_registry();
|
|
1250
1461
|
function resolveProviderApiKey(input) {
|
|
1251
1462
|
const { provider, secrets, dependentSkills } = input;
|
|
1252
|
-
const
|
|
1253
|
-
|
|
1254
|
-
|
|
1463
|
+
const descriptor = getLlmProvider(provider);
|
|
1464
|
+
if (!descriptor) {
|
|
1465
|
+
const skillList2 = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
|
|
1466
|
+
return {
|
|
1467
|
+
error: `Provider "${provider}" is not registered (required by skill(s): ${skillList2}).`
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
const perProviderValue = secrets.llm_api_keys?.[provider];
|
|
1471
|
+
if (typeof perProviderValue === "string" && perProviderValue.length > 0) {
|
|
1255
1472
|
return { apiKey: perProviderValue, origin: "secrets-per-provider" };
|
|
1256
1473
|
}
|
|
1257
|
-
const
|
|
1258
|
-
const envValue = process.env[envVar];
|
|
1474
|
+
const envValue = process.env[descriptor.envVar];
|
|
1259
1475
|
if (envValue) {
|
|
1260
1476
|
return { apiKey: envValue, origin: "env" };
|
|
1261
1477
|
}
|
|
1262
1478
|
const skillList = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
|
|
1263
1479
|
return {
|
|
1264
|
-
error: `Provider "${provider}" needs an API key (required by skill(s): ${skillList}). Set secrets.${
|
|
1480
|
+
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
1481
|
};
|
|
1266
1482
|
}
|
|
1267
1483
|
function resolveLevel(options) {
|
|
@@ -2643,7 +2859,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2643
2859
|
if (!apiKey) {
|
|
2644
2860
|
continue;
|
|
2645
2861
|
}
|
|
2646
|
-
const envVar = provider
|
|
2862
|
+
const envVar = getLlmProvider(provider)?.envVar ?? `${provider.toUpperCase()}_API_KEY`;
|
|
2647
2863
|
process.stdout.write(` Verifying ${provider} API key... `);
|
|
2648
2864
|
const verification = await verifyLlmApiKey(provider, apiKey);
|
|
2649
2865
|
if (verification.ok) {
|