@elisym/cli 0.8.2 → 0.9.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/README.md CHANGED
@@ -169,6 +169,9 @@ then return a concise overview and key points.
169
169
  | `script_timeout_ms` | no | number | Hard timeout for the script in ms (positive integer). Default: `60000`. Script modes only. |
170
170
  | `tools` | no | object[] | External scripts the LLM can call via tool-use. `mode: llm` only. |
171
171
  | `max_tool_rounds` | no | number | Max LLM-tool interaction rounds per job. Default: `10`. `mode: llm` only. |
172
+ | `provider` | no | string | Per-skill LLM provider override (`anthropic` or `openai`). Must be set together with `model`. Falls back to agent-level `llm.provider`. `mode: llm` only. |
173
+ | `model` | no | string | Per-skill model override (e.g. `gpt-5-mini`, `claude-haiku-4-5-20251001`). Must be set together with `provider`. Falls back to agent-level `llm.model`. `mode: llm` only. |
174
+ | `max_tokens` | no | number | Per-skill max-tokens override (positive integer, max 200000). Independent of `provider`/`model`. Falls back to agent-level `llm.max_tokens`, then `4096`. `mode: llm` only. |
172
175
 
173
176
  ### Tool definition
174
177
 
@@ -206,6 +209,12 @@ Everything after the closing `---` becomes the LLM system prompt. Describe the a
206
209
 
207
210
  When **every** loaded skill is non-LLM, `npx @elisym/cli start` skips the LLM-key check - a fully non-AI provider can run with no `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` configured.
208
211
 
212
+ ### Per-skill model / provider overrides
213
+
214
+ Each `mode: llm` skill can route to a different model than the agent default. Declare `provider` + `model` (all-or-nothing) and/or `max_tokens` in the skill's frontmatter; whatever the skill omits inherits from `elisym.yaml` `llm`. If every LLM skill overrides fully, the agent-level `llm` block can be omitted entirely.
215
+
216
+ API keys live in `secrets.anthropic_api_key` / `secrets.openai_api_key` (encrypted at rest when a passphrase is set), or come from the matching `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` env var. `npx @elisym/cli init` and `profile` let you configure both keys, so a skill that overrides to a non-default provider can ship alongside the agent default. See [`skills-examples/cheap-summarizer/`](./skills-examples/cheap-summarizer/SKILL.md) for a working example.
217
+
209
218
  Minimal non-LLM examples (drop `mode` to fall back to `llm`):
210
219
 
211
220
  ```markdown
package/dist/index.js CHANGED
@@ -7,7 +7,6 @@ import { isAddress, createSolanaRpc, address } from '@solana/kit';
7
7
  import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
8
8
  import YAML from 'yaml';
9
9
  import { Command } from 'commander';
10
- import { isEncrypted } from '@elisym/sdk/node';
11
10
  import { createHash } from 'node:crypto';
12
11
  import { lookup } from 'node:dns/promises';
13
12
  import { Socket } from 'node:net';
@@ -140,16 +139,16 @@ async function cmdInit(nameArg, options = {}) {
140
139
  yaml = result.yaml;
141
140
  promptedApiKey = result.apiKey;
142
141
  }
143
- let llmApiKey;
142
+ let defaultProviderKey;
144
143
  if (yaml.llm) {
145
144
  const envKey = yaml.llm.provider === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
146
145
  if (envKey) {
147
- llmApiKey = envKey;
146
+ defaultProviderKey = envKey;
148
147
  console.log(
149
148
  ` Using ${yaml.llm.provider === "anthropic" ? "ANTHROPIC" : "OPENAI"}_API_KEY from environment.`
150
149
  );
151
150
  } else if (promptedApiKey) {
152
- llmApiKey = promptedApiKey;
151
+ defaultProviderKey = promptedApiKey;
153
152
  } else {
154
153
  const { apiKey } = await inquirer.prompt([
155
154
  {
@@ -159,7 +158,38 @@ async function cmdInit(nameArg, options = {}) {
159
158
  mask: "*"
160
159
  }
161
160
  ]);
162
- llmApiKey = apiKey || void 0;
161
+ defaultProviderKey = apiKey || void 0;
162
+ }
163
+ }
164
+ let otherProviderKey;
165
+ if (yaml.llm && !template) {
166
+ const otherProvider = yaml.llm.provider === "anthropic" ? "openai" : "anthropic";
167
+ const otherProviderLabel = otherProvider === "anthropic" ? "Anthropic" : "OpenAI";
168
+ const otherEnvVar = otherProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
169
+ const otherEnvKey = process.env[otherEnvVar];
170
+ const { configureOther } = await inquirer.prompt([
171
+ {
172
+ type: "confirm",
173
+ name: "configureOther",
174
+ message: `Configure ${otherProviderLabel} API key too (for skills that override the default provider)?`,
175
+ default: Boolean(otherEnvKey)
176
+ }
177
+ ]);
178
+ if (configureOther) {
179
+ if (otherEnvKey) {
180
+ otherProviderKey = otherEnvKey;
181
+ console.log(` Using ${otherEnvVar} from environment.`);
182
+ } else {
183
+ const { promptedOther } = await inquirer.prompt([
184
+ {
185
+ type: "password",
186
+ name: "promptedOther",
187
+ message: `${otherProviderLabel} API key:`,
188
+ mask: "*"
189
+ }
190
+ ]);
191
+ otherProviderKey = promptedOther || void 0;
192
+ }
163
193
  }
164
194
  }
165
195
  let passphrase = "";
@@ -194,12 +224,15 @@ async function cmdInit(nameArg, options = {}) {
194
224
  const nostrPubkey = getPublicKey(nostrSecretBytes);
195
225
  const nostrSecretHex = Buffer.from(nostrSecretBytes).toString("hex");
196
226
  const created = await createAgentDir({ target, name: agentName, cwd });
227
+ const anthropicKey = yaml.llm?.provider === "anthropic" ? defaultProviderKey : otherProviderKey;
228
+ const openaiKey = yaml.llm?.provider === "openai" ? defaultProviderKey : otherProviderKey;
197
229
  await writeYaml(created.dir, yaml);
198
230
  await writeSecrets(
199
231
  created.dir,
200
232
  {
201
233
  nostr_secret_key: nostrSecretHex,
202
- llm_api_key: llmApiKey
234
+ anthropic_api_key: anthropicKey,
235
+ openai_api_key: openaiKey
203
236
  },
204
237
  passphrase || void 0
205
238
  );
@@ -490,7 +523,7 @@ async function cmdProfile(name) {
490
523
  {
491
524
  type: "list",
492
525
  name: "llmProvider",
493
- message: "LLM provider:",
526
+ message: "Default LLM provider (used by skills without a `provider:` override):",
494
527
  choices: [
495
528
  { name: "Anthropic (Claude)", value: "anthropic" },
496
529
  { name: "OpenAI (GPT)", value: "openai" }
@@ -498,16 +531,17 @@ async function cmdProfile(name) {
498
531
  default: loaded.yaml.llm?.provider ?? "anthropic"
499
532
  }
500
533
  ]);
534
+ const otherProvider = llmProvider === "anthropic" ? "openai" : "anthropic";
535
+ const defaultKeyStatus = describeKeyStatus(loaded.secrets, llmProvider);
501
536
  const { apiKey } = await inquirer.prompt([
502
537
  {
503
538
  type: "password",
504
539
  name: "apiKey",
505
- message: "API key (leave empty to keep current):",
540
+ message: `${labelFor(llmProvider)} API key [${defaultKeyStatus}] (leave empty to keep current):`,
506
541
  mask: "*"
507
542
  }
508
543
  ]);
509
- const currentKeyPlain = loaded.secrets.llm_api_key && !isEncrypted(loaded.secrets.llm_api_key) ? loaded.secrets.llm_api_key : "";
510
- const probeKey = apiKey || currentKeyPlain;
544
+ const probeKey = apiKey || pickPlainKey(loaded.secrets, llmProvider);
511
545
  console.log(" Fetching available models...");
512
546
  const { fetchModels: fetchModels2 } = await Promise.resolve().then(() => (init_init(), init_exports));
513
547
  const models = await fetchModels2(llmProvider, probeKey);
@@ -528,13 +562,37 @@ async function cmdProfile(name) {
528
562
  default: loaded.yaml.llm?.max_tokens ?? 4096
529
563
  }
530
564
  ]);
565
+ const otherKeyStatus = describeKeyStatus(loaded.secrets, otherProvider);
566
+ const { otherApiKey } = await inquirer.prompt([
567
+ {
568
+ type: "password",
569
+ name: "otherApiKey",
570
+ message: `${labelFor(otherProvider)} API key for skill overrides [${otherKeyStatus}] (leave empty to keep current):`,
571
+ mask: "*"
572
+ }
573
+ ]);
531
574
  const nextLlm = { provider: llmProvider, model, max_tokens: maxTokens };
532
575
  const nextYaml = { ...loaded.yaml, llm: nextLlm };
533
576
  await writeYaml(loaded.dir, nextYaml);
534
577
  loaded.yaml = nextYaml;
535
- if (apiKey) {
536
- await writeSecrets(loaded.dir, { ...loaded.secrets, llm_api_key: apiKey }, passphrase);
537
- loaded.secrets = { ...loaded.secrets, llm_api_key: apiKey };
578
+ if (apiKey || otherApiKey) {
579
+ const nextSecrets = { ...loaded.secrets };
580
+ if (apiKey) {
581
+ if (llmProvider === "anthropic") {
582
+ nextSecrets.anthropic_api_key = apiKey;
583
+ } else {
584
+ nextSecrets.openai_api_key = apiKey;
585
+ }
586
+ }
587
+ if (otherApiKey) {
588
+ if (otherProvider === "anthropic") {
589
+ nextSecrets.anthropic_api_key = otherApiKey;
590
+ } else {
591
+ nextSecrets.openai_api_key = otherApiKey;
592
+ }
593
+ }
594
+ await writeSecrets(loaded.dir, nextSecrets, passphrase);
595
+ loaded.secrets = nextSecrets;
538
596
  }
539
597
  console.log(" LLM updated.\n");
540
598
  }
@@ -548,6 +606,17 @@ function truncate(value, max = 40) {
548
606
  }
549
607
  return value.slice(0, max - 1) + "\u2026";
550
608
  }
609
+ function labelFor(provider) {
610
+ return provider === "anthropic" ? "Anthropic" : "OpenAI";
611
+ }
612
+ function describeKeyStatus(secrets, provider) {
613
+ const perProviderField = provider === "anthropic" ? "anthropic_api_key" : "openai_api_key";
614
+ return secrets[perProviderField] ? "set" : "not set";
615
+ }
616
+ function pickPlainKey(secrets, provider) {
617
+ const perProviderField = provider === "anthropic" ? "anthropic_api_key" : "openai_api_key";
618
+ return secrets[perProviderField] ?? "";
619
+ }
551
620
  var TCP_PROBE_TIMEOUT_MS = 3e3;
552
621
  function parseRelayUrl(url) {
553
622
  try {
@@ -1095,6 +1164,82 @@ var OpenAIClient = class {
1095
1164
  }));
1096
1165
  }
1097
1166
  };
1167
+
1168
+ // src/llm/resolve.ts
1169
+ var DEFAULT_MAX_TOKENS = 4096;
1170
+ function resolveSkillLlm(input, agentDefault) {
1171
+ const override = input.llmOverride;
1172
+ const overridePairSet = override?.provider !== void 0 && override.model !== void 0;
1173
+ let provider;
1174
+ let model;
1175
+ if (overridePairSet) {
1176
+ provider = override.provider;
1177
+ model = override.model;
1178
+ } else if (agentDefault) {
1179
+ provider = agentDefault.provider;
1180
+ model = agentDefault.model;
1181
+ }
1182
+ if (!provider || !model) {
1183
+ return {
1184
+ 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>'.`
1185
+ };
1186
+ }
1187
+ const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS;
1188
+ return { provider, model, maxTokens };
1189
+ }
1190
+
1191
+ // src/llm/cache.ts
1192
+ function cacheKeyFor(triple) {
1193
+ return JSON.stringify({
1194
+ provider: triple.provider,
1195
+ model: triple.model,
1196
+ maxTokens: triple.maxTokens
1197
+ });
1198
+ }
1199
+ function resolveTripleForOverride(override, agentDefault) {
1200
+ const overridePairSet = override?.provider !== void 0 && override.model !== void 0;
1201
+ let provider;
1202
+ let model;
1203
+ if (overridePairSet) {
1204
+ provider = override.provider;
1205
+ model = override.model;
1206
+ } else if (agentDefault) {
1207
+ provider = agentDefault.provider;
1208
+ model = agentDefault.model;
1209
+ }
1210
+ if (!provider || !model) {
1211
+ return void 0;
1212
+ }
1213
+ const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS;
1214
+ return { provider, model, maxTokens };
1215
+ }
1216
+
1217
+ // src/llm/keys.ts
1218
+ var ENV_VAR_FOR_PROVIDER = {
1219
+ anthropic: "ANTHROPIC_API_KEY",
1220
+ openai: "OPENAI_API_KEY"
1221
+ };
1222
+ var SECRET_FIELD_FOR_PROVIDER = {
1223
+ anthropic: "anthropic_api_key",
1224
+ openai: "openai_api_key"
1225
+ };
1226
+ function resolveProviderApiKey(input) {
1227
+ const { provider, secrets, dependentSkills } = input;
1228
+ const perProviderField = SECRET_FIELD_FOR_PROVIDER[provider];
1229
+ const perProviderValue = secrets[perProviderField];
1230
+ if (perProviderValue) {
1231
+ return { apiKey: perProviderValue, origin: "secrets-per-provider" };
1232
+ }
1233
+ const envVar = ENV_VAR_FOR_PROVIDER[provider];
1234
+ const envValue = process.env[envVar];
1235
+ if (envValue) {
1236
+ return { apiKey: envValue, origin: "env" };
1237
+ }
1238
+ const skillList = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
1239
+ return {
1240
+ error: `Provider "${provider}" needs an API key (required by skill(s): ${skillList}). Set secrets.${perProviderField} via 'npx @elisym/cli profile <agent>' or export ${envVar}.`
1241
+ };
1242
+ }
1098
1243
  function resolveLevel(options) {
1099
1244
  if (options.level) {
1100
1245
  return options.level;
@@ -1879,11 +2024,12 @@ var ScriptSkill = class {
1879
2024
  priceSubunits;
1880
2025
  asset;
1881
2026
  mode = "llm";
2027
+ llmOverride;
1882
2028
  image;
1883
2029
  imageFile;
1884
2030
  dir;
1885
2031
  inner;
1886
- constructor(name, description, capabilities, priceSubunits, assetOrImage, imageOrImageFile, imageFileOrDir, dirOrPrompt, promptOrTools, toolsOrRounds, rounds) {
2032
+ constructor(name, description, capabilities, priceSubunits, assetOrImage, imageOrImageFile, imageFileOrDir, dirOrPrompt, promptOrTools, toolsOrRounds, rounds, llmOverride) {
1887
2033
  let asset;
1888
2034
  let image;
1889
2035
  let imageFile;
@@ -1913,6 +2059,7 @@ var ScriptSkill = class {
1913
2059
  this.capabilities = capabilities;
1914
2060
  this.priceSubunits = priceSubunits;
1915
2061
  this.asset = asset;
2062
+ this.llmOverride = llmOverride;
1916
2063
  this.image = image;
1917
2064
  this.imageFile = imageFile;
1918
2065
  this.dir = skillDir;
@@ -1926,6 +2073,7 @@ var ScriptSkill = class {
1926
2073
  systemPrompt,
1927
2074
  tools,
1928
2075
  maxToolRounds,
2076
+ llmOverride,
1929
2077
  image,
1930
2078
  imageFile
1931
2079
  });
@@ -1950,7 +2098,8 @@ function buildCliSkill(parsed, entryPath) {
1950
2098
  entryPath,
1951
2099
  parsed.systemPrompt,
1952
2100
  parsed.tools,
1953
- parsed.maxToolRounds
2101
+ parsed.maxToolRounds,
2102
+ parsed.llmOverride
1954
2103
  );
1955
2104
  case "static-file": {
1956
2105
  if (parsed.outputFile === void 0) {
@@ -2036,7 +2185,7 @@ function loadSkillsFromDir(skillsDir) {
2036
2185
  }
2037
2186
  return skills;
2038
2187
  }
2039
- function isEncrypted2(event) {
2188
+ function isEncrypted(event) {
2040
2189
  return event.tags.some((t) => t[0] === "encrypted" && t[1] === "nip44");
2041
2190
  }
2042
2191
  var HEALTH_CHECK_IDLE_MS = 30 * 60 * 1e3;
@@ -2070,7 +2219,7 @@ var NostrTransport = class {
2070
2219
  }
2071
2220
  const tags = event.tags.filter((t) => t[0] === "t").map((t) => t[1]);
2072
2221
  const bidTag = event.tags.find((t) => t[0] === "bid");
2073
- const encrypted = isEncrypted2(event);
2222
+ const encrypted = isEncrypted(event);
2074
2223
  const iTag = event.tags.find((t) => t[0] === "i");
2075
2224
  let inputType = "text";
2076
2225
  if (iTag?.[2]) {
@@ -2399,62 +2548,124 @@ async function cmdStart(nameArg, options = {}) {
2399
2548
  );
2400
2549
  process.exit(1);
2401
2550
  }
2402
- const hasLlmSkill = allSkills.some((skill) => skill.mode === "llm");
2403
- let llm;
2404
- if (hasLlmSkill) {
2405
- const llmSkillNames = allSkills.filter((skill) => skill.mode === "llm").map((skill) => skill.name);
2406
- if (!loaded.yaml.llm) {
2407
- console.error(
2408
- ` ! No LLM configured, but ${llmSkillNames.length} skill(s) require it: ${llmSkillNames.join(", ")}.`
2409
- );
2410
- console.error(
2411
- ` Add an LLM provider via \`npx @elisym/cli profile ${agentName}\`, or remove those skills from ${skillsDir}.
2412
- `
2551
+ const llmSkills = allSkills.filter((skill) => skill.mode === "llm");
2552
+ const triplesByKey = /* @__PURE__ */ new Map();
2553
+ const dependentSkillsByProvider = /* @__PURE__ */ new Map();
2554
+ let agentDefaultCacheKey;
2555
+ const llmClientCache = /* @__PURE__ */ new Map();
2556
+ if (llmSkills.length > 0) {
2557
+ const resolutionErrors = [];
2558
+ for (const skill of llmSkills) {
2559
+ const skillMdPath = join(skillsDir, skill.name, "SKILL.md");
2560
+ const result = resolveSkillLlm(
2561
+ { skillName: skill.name, skillMdPath, llmOverride: skill.llmOverride },
2562
+ loaded.yaml.llm
2413
2563
  );
2564
+ if ("error" in result) {
2565
+ resolutionErrors.push(result.error);
2566
+ continue;
2567
+ }
2568
+ const cacheKey = cacheKeyFor(result);
2569
+ triplesByKey.set(cacheKey, result);
2570
+ const list = dependentSkillsByProvider.get(result.provider) ?? [];
2571
+ list.push(skill.name);
2572
+ dependentSkillsByProvider.set(result.provider, list);
2573
+ }
2574
+ if (resolutionErrors.length > 0) {
2575
+ for (const message of resolutionErrors) {
2576
+ console.error(` ! ${message}`);
2577
+ }
2578
+ console.error("");
2414
2579
  process.exit(1);
2415
2580
  }
2416
- if (!loaded.secrets.llm_api_key) {
2417
- const keyEnvVar2 = loaded.yaml.llm.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2418
- console.error(` ! No LLM API key (required by skill(s): ${llmSkillNames.join(", ")}).`);
2419
- console.error(
2420
- ` Set ${keyEnvVar2} or update the agent's secrets via \`npx @elisym/cli profile ${agentName}\`.
2421
- `
2422
- );
2581
+ if (loaded.yaml.llm) {
2582
+ const agentDefaultTriple = {
2583
+ provider: loaded.yaml.llm.provider,
2584
+ model: loaded.yaml.llm.model,
2585
+ maxTokens: loaded.yaml.llm.max_tokens
2586
+ };
2587
+ const agentKey = cacheKeyFor(agentDefaultTriple);
2588
+ if (triplesByKey.has(agentKey)) {
2589
+ agentDefaultCacheKey = agentKey;
2590
+ }
2591
+ }
2592
+ const keyByProvider = /* @__PURE__ */ new Map();
2593
+ const keyErrors = [];
2594
+ for (const [provider, dependents] of dependentSkillsByProvider) {
2595
+ const keyResult = resolveProviderApiKey({
2596
+ provider,
2597
+ secrets: loaded.secrets,
2598
+ dependentSkills: dependents
2599
+ });
2600
+ if ("error" in keyResult) {
2601
+ keyErrors.push(keyResult.error);
2602
+ continue;
2603
+ }
2604
+ keyByProvider.set(provider, keyResult.apiKey);
2605
+ }
2606
+ if (keyErrors.length > 0) {
2607
+ for (const message of keyErrors) {
2608
+ console.error(` ! ${message}`);
2609
+ }
2610
+ console.error("");
2423
2611
  process.exit(1);
2424
2612
  }
2425
- const llmProvider = loaded.yaml.llm.provider;
2426
- const keyEnvVar = llmProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2427
- process.stdout.write(` Verifying ${llmProvider} API key... `);
2428
- const verification = await verifyLlmApiKey(llmProvider, loaded.secrets.llm_api_key);
2429
- if (verification.ok) {
2430
- console.log("ok");
2431
- } else if (verification.reason === "invalid") {
2432
- console.log("INVALID");
2433
- console.error(` ! ${llmProvider} rejected the API key (HTTP ${verification.status}).`);
2434
- console.error(
2435
- ` Update it via \`npx @elisym/cli profile\` or set ${keyEnvVar} to a valid key.
2613
+ for (const provider of keyByProvider.keys()) {
2614
+ const apiKey = keyByProvider.get(provider);
2615
+ if (!apiKey) {
2616
+ continue;
2617
+ }
2618
+ const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2619
+ process.stdout.write(` Verifying ${provider} API key... `);
2620
+ const verification = await verifyLlmApiKey(provider, apiKey);
2621
+ if (verification.ok) {
2622
+ console.log("ok");
2623
+ } else if (verification.reason === "invalid") {
2624
+ console.log("INVALID");
2625
+ console.error(` ! ${provider} rejected the API key (HTTP ${verification.status}).`);
2626
+ console.error(
2627
+ ` Update it via \`npx @elisym/cli profile ${agentName}\` or set ${envVar} to a valid key.
2436
2628
  `
2437
- );
2438
- process.exit(1);
2439
- } else {
2440
- console.log("unavailable");
2441
- console.warn(
2442
- ` ! Could not verify ${llmProvider} API key (${verification.error}). Continuing - jobs will fail if the key is invalid.
2629
+ );
2630
+ process.exit(1);
2631
+ } else {
2632
+ console.log("unavailable");
2633
+ console.warn(
2634
+ ` ! Could not verify ${provider} API key (${verification.error}). Continuing - jobs will fail if the key is invalid.
2443
2635
  `
2444
- );
2636
+ );
2637
+ }
2638
+ }
2639
+ for (const [cacheKey, triple] of triplesByKey) {
2640
+ const apiKey = keyByProvider.get(triple.provider);
2641
+ if (!apiKey) {
2642
+ console.error(` ! Internal error: no API key for provider "${triple.provider}".
2643
+ `);
2644
+ process.exit(1);
2645
+ }
2646
+ const config = {
2647
+ provider: triple.provider,
2648
+ apiKey,
2649
+ model: triple.model,
2650
+ maxTokens: triple.maxTokens,
2651
+ logUsage: true
2652
+ };
2653
+ llmClientCache.set(cacheKey, createLlmClient(config));
2445
2654
  }
2446
- llm = createLlmClient({
2447
- provider: loaded.yaml.llm.provider,
2448
- apiKey: loaded.secrets.llm_api_key,
2449
- model: loaded.yaml.llm.model,
2450
- maxTokens: loaded.yaml.llm.max_tokens,
2451
- logUsage: true
2452
- });
2453
2655
  } else {
2454
2656
  console.log(" No LLM skills loaded; skipping LLM key check.\n");
2455
2657
  }
2658
+ const agentDefaultClient = agentDefaultCacheKey !== void 0 ? llmClientCache.get(agentDefaultCacheKey) : void 0;
2659
+ const getLlm = (override) => {
2660
+ const triple = resolveTripleForOverride(override, loaded.yaml.llm);
2661
+ if (!triple) {
2662
+ return void 0;
2663
+ }
2664
+ return llmClientCache.get(cacheKeyFor(triple));
2665
+ };
2456
2666
  const skillCtx = {
2457
- llm,
2667
+ llm: agentDefaultClient,
2668
+ getLlm,
2458
2669
  agentName,
2459
2670
  agentDescription: loaded.yaml.description ?? ""
2460
2671
  };
@@ -2754,9 +2965,9 @@ async function loadAgentWithPrompt(name, cwd) {
2754
2965
  try {
2755
2966
  return await loadAgent(name, cwd, envPassphrase);
2756
2967
  } catch (e) {
2757
- const isEncrypted3 = /encrypted secrets/i.test(e?.message ?? "");
2968
+ const isEncrypted2 = /encrypted secrets/i.test(e?.message ?? "");
2758
2969
  const isWrongPassphrase = /Decryption failed/i.test(e?.message ?? "");
2759
- if (!isEncrypted3 && !isWrongPassphrase) {
2970
+ if (!isEncrypted2 && !isWrongPassphrase) {
2760
2971
  throw e;
2761
2972
  }
2762
2973
  if (!process.stdin.isTTY) {