@elisym/sdk 0.12.5 → 0.14.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/skills.d.ts CHANGED
@@ -16,9 +16,28 @@ interface SkillOutput {
16
16
  data: string;
17
17
  outputMime?: string;
18
18
  }
19
+ /**
20
+ * Optional per-skill LLM override declared in SKILL.md frontmatter.
21
+ *
22
+ * Parse-time invariant (enforced by `validateLlmOverride` in the loader):
23
+ * `provider` is set iff `model` is set. `maxTokens` is independent. So
24
+ * downstream code may rely on: when `provider !== undefined`, `model` is
25
+ * also defined.
26
+ */
27
+ interface SkillLlmOverride {
28
+ provider?: string;
29
+ model?: string;
30
+ maxTokens?: number;
31
+ }
19
32
  interface SkillContext {
20
- /** Required only when the routed skill has `mode === 'llm'`. */
33
+ /** Agent-default LLM client. May be undefined when every LLM skill overrides. */
21
34
  llm?: LlmClient;
35
+ /**
36
+ * Resolve the LLM client for a skill. The runtime caches clients by
37
+ * resolved (provider, model, maxTokens) triple. Callers pass their
38
+ * `llmOverride` (or undefined for the agent default).
39
+ */
40
+ getLlm?: (override?: SkillLlmOverride) => LlmClient | undefined;
22
41
  agentName: string;
23
42
  agentDescription: string;
24
43
  signal?: AbortSignal;
@@ -64,6 +83,14 @@ interface LlmClient {
64
83
  completeWithTools(systemPrompt: string, messages: unknown[], tools: ToolDef[], signal?: AbortSignal): Promise<CompletionResult>;
65
84
  formatToolResultMessages(results: ToolResult[]): unknown[];
66
85
  }
86
+ /** Provider id. Plain string so the registry stays open to additions without type-union edits. */
87
+ type LlmProvider = string;
88
+ interface LlmClientConfig {
89
+ provider: LlmProvider;
90
+ apiKey: string;
91
+ model?: string;
92
+ maxTokens?: number;
93
+ }
67
94
  interface Skill {
68
95
  name: string;
69
96
  description: string;
@@ -74,22 +101,17 @@ interface Skill {
74
101
  asset: Asset;
75
102
  /** Execution mode. Default 'llm' for back-compat. */
76
103
  mode: SkillMode;
104
+ /**
105
+ * Optional per-skill LLM config override (only set when mode === 'llm').
106
+ * Carried through from SKILL.md frontmatter so the runtime can route this
107
+ * skill to a non-default model/provider/max_tokens.
108
+ */
109
+ llmOverride?: SkillLlmOverride;
77
110
  image?: string;
78
111
  imageFile?: string;
79
112
  execute(input: SkillInput, ctx: SkillContext): Promise<SkillOutput>;
80
113
  }
81
114
 
82
- type LlmProvider = 'anthropic' | 'openai';
83
- interface LlmClientConfig {
84
- provider: LlmProvider;
85
- apiKey: string;
86
- model?: string;
87
- maxTokens?: number;
88
- }
89
- declare function createAnthropicClient(config: Omit<LlmClientConfig, 'provider'>): LlmClient;
90
- declare function createOpenAIClient(config: Omit<LlmClientConfig, 'provider'>): LlmClient;
91
- declare function createLlmClient(config: LlmClientConfig): LlmClient;
92
-
93
115
  declare const MAX_SCRIPT_OUTPUT = 1000000;
94
116
  declare const DEFAULT_SCRIPT_TIMEOUT_MS = 60000;
95
117
  interface SkillToolDef {
@@ -144,6 +166,8 @@ interface ScriptSkillParams {
144
166
  systemPrompt: string;
145
167
  tools: SkillToolDef[];
146
168
  maxToolRounds: number;
169
+ /** Optional per-skill LLM override (provider/model pair and/or maxTokens). */
170
+ llmOverride?: SkillLlmOverride;
147
171
  image?: string;
148
172
  imageFile?: string;
149
173
  logger?: ScriptSkillLogger;
@@ -162,6 +186,7 @@ declare class ScriptSkill implements Skill {
162
186
  priceSubunits: bigint;
163
187
  asset: Asset;
164
188
  mode: SkillMode;
189
+ readonly llmOverride?: SkillLlmOverride;
165
190
  image?: string;
166
191
  imageFile?: string;
167
192
  private skillDir;
@@ -171,6 +196,18 @@ declare class ScriptSkill implements Skill {
171
196
  private logger;
172
197
  constructor(params: ScriptSkillParams);
173
198
  execute(input: SkillInput, ctx: SkillContext): Promise<SkillOutput>;
199
+ /**
200
+ * Resolve the LLM client for this skill from the runtime context.
201
+ *
202
+ * Contract:
203
+ * - When `llmOverride` is set, `ctx.getLlm` MUST be wired. Falling back to
204
+ * `ctx.llm` (the agent default) would silently use the wrong configuration
205
+ * for max-tokens-only overrides.
206
+ * - When no override is set, prefer `ctx.getLlm()` (returns the agent
207
+ * default), then fall back to `ctx.llm` for legacy callers that wire only
208
+ * a single client.
209
+ */
210
+ private resolveLlmClient;
174
211
  private runTool;
175
212
  }
176
213
 
@@ -304,6 +341,12 @@ interface SkillFrontmatter {
304
341
  image_file?: unknown;
305
342
  tools?: unknown;
306
343
  max_tool_rounds?: unknown;
344
+ /** Optional per-skill LLM provider override (e.g. 'anthropic', 'openai'). Pairs with `model`. */
345
+ provider?: unknown;
346
+ /** Optional per-skill LLM model override. Pairs with `provider`. */
347
+ model?: unknown;
348
+ /** Optional per-skill max_tokens override. Independent of provider/model. */
349
+ max_tokens?: unknown;
307
350
  /** Execution mode. Default 'llm'. */
308
351
  mode?: unknown;
309
352
  /** Required when mode === 'static-file'. Path relative to skill dir. */
@@ -326,6 +369,12 @@ interface ParsedSkill {
326
369
  systemPrompt: string;
327
370
  tools: SkillToolDef[];
328
371
  maxToolRounds: number;
372
+ /**
373
+ * Per-skill LLM override (only present when mode === 'llm' and the SKILL.md
374
+ * declared at least one of `provider`/`model`/`max_tokens`). Parse-time
375
+ * invariant: `provider` set iff `model` set.
376
+ */
377
+ llmOverride?: SkillLlmOverride;
329
378
  image?: string;
330
379
  imageFile?: string;
331
380
  /** Set when mode === 'static-file'. */
@@ -362,4 +411,4 @@ declare function validateSkillFrontmatter(frontmatter: SkillFrontmatter, systemP
362
411
  */
363
412
  declare function loadSkillsFromDir(skillsDir: string, options?: LoadSkillsOptions): Skill[];
364
413
 
365
- export { type CompletionResult, DEFAULT_MAX_TOOL_ROUNDS, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill, type DynamicScriptSkillParams, type LlmClient, type LlmClientConfig, type LlmProvider, type LoadSkillsOptions, type LoaderLogger, MAX_SCRIPT_OUTPUT, MAX_STATIC_FILE_SIZE, type ParsedSkill, type RunScriptOptions, type RunScriptResult, ScriptSkill, type ScriptSkillLogger, type ScriptSkillParams, type Skill, type SkillContext, type SkillFrontmatter, type SkillInput, type SkillMode, type SkillOutput, type SkillToolDef, StaticFileSkill, type StaticFileSkillParams, StaticScriptSkill, type StaticScriptSkillParams, type ToolCall, type ToolDef, type ToolResult, createAnthropicClient, createLlmClient, createOpenAIClient, loadSkillsFromDir, parseSkillMd, resolveInsidePath, runScript, validateSkillFrontmatter };
414
+ export { type CompletionResult, DEFAULT_MAX_TOOL_ROUNDS, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill, type DynamicScriptSkillParams, type LlmClient, type LlmClientConfig, type LlmProvider, type LoadSkillsOptions, type LoaderLogger, MAX_SCRIPT_OUTPUT, MAX_STATIC_FILE_SIZE, type ParsedSkill, type RunScriptOptions, type RunScriptResult, ScriptSkill, type ScriptSkillLogger, type ScriptSkillParams, type Skill, type SkillContext, type SkillFrontmatter, type SkillInput, type SkillLlmOverride, type SkillMode, type SkillOutput, type SkillToolDef, StaticFileSkill, type StaticFileSkillParams, StaticScriptSkill, type StaticScriptSkillParams, type ToolCall, type ToolDef, type ToolResult, loadSkillsFromDir, parseSkillMd, resolveInsidePath, runScript, validateSkillFrontmatter };
package/dist/skills.js CHANGED
@@ -6,303 +6,7 @@ import { readdirSync, statSync, readFileSync } from 'node:fs';
6
6
  import YAML from 'yaml';
7
7
  import Decimal from 'decimal.js-light';
8
8
 
9
- // src/skills/llmClient.ts
10
- var LLM_TIMEOUT_MS = 12e4;
11
- var MAX_RETRIES = 2;
12
- var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
13
- var DEFAULT_MAX_TOKENS = 4096;
14
- var DEFAULT_ANTHROPIC_MODEL = "claude-haiku-4-5-20251001";
15
- var DEFAULT_OPENAI_MODEL = "gpt-4o-mini";
16
- function createAbortError() {
17
- const err = new Error("The operation was aborted");
18
- err.name = "AbortError";
19
- return err;
20
- }
21
- function sleepWithSignal(ms, signal) {
22
- if (signal?.aborted) {
23
- return Promise.reject(createAbortError());
24
- }
25
- if (!signal) {
26
- return new Promise((resolve2) => setTimeout(resolve2, ms));
27
- }
28
- return new Promise((resolve2, reject) => {
29
- const cleanup = () => {
30
- clearTimeout(timer);
31
- signal.removeEventListener("abort", onAbort);
32
- };
33
- const onAbort = () => {
34
- cleanup();
35
- reject(createAbortError());
36
- };
37
- const timer = setTimeout(() => {
38
- cleanup();
39
- resolve2();
40
- }, ms);
41
- signal.addEventListener("abort", onAbort, { once: true });
42
- });
43
- }
44
- async function fetchWithTimeout(url, init, signal) {
45
- if (signal?.aborted) {
46
- throw createAbortError();
47
- }
48
- const controller = new AbortController();
49
- const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
50
- const onAbort = () => controller.abort();
51
- signal?.addEventListener("abort", onAbort, { once: true });
52
- try {
53
- return await fetch(url, { ...init, signal: controller.signal });
54
- } finally {
55
- clearTimeout(timer);
56
- signal?.removeEventListener("abort", onAbort);
57
- }
58
- }
59
- async function fetchWithRetry(url, init, signal) {
60
- for (let attempt = 0; ; attempt++) {
61
- let response;
62
- try {
63
- response = await fetchWithTimeout(url, init, signal);
64
- } catch (error) {
65
- const name = error instanceof Error ? error.name : "";
66
- if (attempt >= MAX_RETRIES || name === "AbortError") {
67
- throw error;
68
- }
69
- await sleepWithSignal(Math.min(1e3 * 2 ** attempt, 8e3), signal);
70
- continue;
71
- }
72
- if (response.ok || attempt >= MAX_RETRIES || !RETRYABLE_STATUSES.has(response.status)) {
73
- return response;
74
- }
75
- const retryAfter = response.headers.get("retry-after");
76
- const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3 || 1e3 * 2 ** attempt, 3e4) : Math.min(1e3 * 2 ** attempt, 8e3);
77
- await response.body?.cancel().catch(() => void 0);
78
- await sleepWithSignal(delay, signal);
79
- }
80
- }
81
- var AnthropicClient = class {
82
- constructor(config) {
83
- this.config = config;
84
- }
85
- async complete(systemPrompt, userInput, signal) {
86
- const response = await fetchWithRetry(
87
- "https://api.anthropic.com/v1/messages",
88
- {
89
- method: "POST",
90
- headers: {
91
- "Content-Type": "application/json",
92
- "x-api-key": this.config.apiKey,
93
- "anthropic-version": "2023-06-01"
94
- },
95
- body: JSON.stringify({
96
- model: this.config.model,
97
- max_tokens: this.config.maxTokens,
98
- system: systemPrompt,
99
- messages: [{ role: "user", content: userInput }]
100
- })
101
- },
102
- signal
103
- );
104
- if (!response.ok) {
105
- throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
106
- }
107
- const data = await response.json();
108
- const textBlock = data.content?.find((block) => block.type === "text");
109
- return textBlock?.text ?? "";
110
- }
111
- async completeWithTools(systemPrompt, messages, tools, signal) {
112
- const anthropicTools = tools.map((tool) => ({
113
- name: tool.name,
114
- description: tool.description,
115
- input_schema: {
116
- type: "object",
117
- properties: Object.fromEntries(
118
- tool.parameters.map((param) => [
119
- param.name,
120
- { type: "string", description: param.description }
121
- ])
122
- ),
123
- required: tool.parameters.filter((param) => param.required).map((param) => param.name)
124
- }
125
- }));
126
- const response = await fetchWithRetry(
127
- "https://api.anthropic.com/v1/messages",
128
- {
129
- method: "POST",
130
- headers: {
131
- "Content-Type": "application/json",
132
- "x-api-key": this.config.apiKey,
133
- "anthropic-version": "2023-06-01"
134
- },
135
- body: JSON.stringify({
136
- model: this.config.model,
137
- max_tokens: this.config.maxTokens,
138
- system: systemPrompt,
139
- messages,
140
- tools: anthropicTools
141
- })
142
- },
143
- signal
144
- );
145
- if (!response.ok) {
146
- throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
147
- }
148
- const data = await response.json();
149
- const content = data.content ?? [];
150
- const toolUses = content.filter((block) => block.type === "tool_use");
151
- if (toolUses.length > 0) {
152
- const calls = toolUses.map((block) => ({
153
- id: block.id ?? "",
154
- name: block.name ?? "",
155
- arguments: block.input ?? {}
156
- }));
157
- return {
158
- type: "tool_use",
159
- calls,
160
- assistantMessage: { role: "assistant", content }
161
- };
162
- }
163
- const textBlock = content.find((block) => block.type === "text");
164
- return { type: "text", text: textBlock?.text ?? "" };
165
- }
166
- formatToolResultMessages(results) {
167
- return [
168
- {
169
- role: "user",
170
- content: results.map((result) => ({
171
- type: "tool_result",
172
- tool_use_id: result.callId,
173
- content: result.content
174
- }))
175
- }
176
- ];
177
- }
178
- };
179
- var OpenAIClient = class {
180
- constructor(config) {
181
- this.config = config;
182
- }
183
- isReasoningModel() {
184
- return /^o\d/.test(this.config.model);
185
- }
186
- async complete(systemPrompt, userInput, signal) {
187
- const reasoning = this.isReasoningModel();
188
- const response = await fetchWithRetry(
189
- "https://api.openai.com/v1/chat/completions",
190
- {
191
- method: "POST",
192
- headers: {
193
- "Content-Type": "application/json",
194
- Authorization: `Bearer ${this.config.apiKey}`
195
- },
196
- body: JSON.stringify({
197
- model: this.config.model,
198
- ...reasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
199
- messages: [
200
- { role: reasoning ? "developer" : "system", content: systemPrompt },
201
- { role: "user", content: userInput }
202
- ]
203
- })
204
- },
205
- signal
206
- );
207
- if (!response.ok) {
208
- throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
209
- }
210
- const data = await response.json();
211
- return data.choices?.[0]?.message?.content ?? "";
212
- }
213
- async completeWithTools(systemPrompt, messages, tools, signal) {
214
- const openaiTools = tools.map((tool) => ({
215
- type: "function",
216
- function: {
217
- name: tool.name,
218
- description: tool.description,
219
- parameters: {
220
- type: "object",
221
- properties: Object.fromEntries(
222
- tool.parameters.map((param) => [
223
- param.name,
224
- { type: "string", description: param.description }
225
- ])
226
- ),
227
- required: tool.parameters.filter((param) => param.required).map((param) => param.name)
228
- }
229
- }
230
- }));
231
- const reasoning = this.isReasoningModel();
232
- const response = await fetchWithRetry(
233
- "https://api.openai.com/v1/chat/completions",
234
- {
235
- method: "POST",
236
- headers: {
237
- "Content-Type": "application/json",
238
- Authorization: `Bearer ${this.config.apiKey}`
239
- },
240
- body: JSON.stringify({
241
- model: this.config.model,
242
- ...reasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
243
- messages: [
244
- { role: reasoning ? "developer" : "system", content: systemPrompt },
245
- ...messages
246
- ],
247
- tools: openaiTools
248
- })
249
- },
250
- signal
251
- );
252
- if (!response.ok) {
253
- throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
254
- }
255
- const data = await response.json();
256
- const message = data.choices?.[0]?.message;
257
- const toolCalls = message?.tool_calls ?? [];
258
- if (toolCalls.length > 0) {
259
- const calls = toolCalls.map((call) => {
260
- let args;
261
- try {
262
- args = JSON.parse(call.function?.arguments ?? "{}");
263
- } catch {
264
- args = {};
265
- }
266
- return { id: call.id ?? "", name: call.function?.name ?? "", arguments: args };
267
- });
268
- return { type: "tool_use", calls, assistantMessage: message };
269
- }
270
- return { type: "text", text: message?.content ?? "" };
271
- }
272
- formatToolResultMessages(results) {
273
- return results.map((result) => ({
274
- role: "tool",
275
- tool_call_id: result.callId,
276
- content: result.content
277
- }));
278
- }
279
- };
280
- function createAnthropicClient(config) {
281
- if (!config.apiKey) {
282
- throw new Error("ANTHROPIC_API_KEY is required for skill runtime");
283
- }
284
- return new AnthropicClient({
285
- apiKey: config.apiKey,
286
- model: config.model ?? DEFAULT_ANTHROPIC_MODEL,
287
- maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS
288
- });
289
- }
290
- function createOpenAIClient(config) {
291
- if (!config.apiKey) {
292
- throw new Error("OPENAI_API_KEY is required for skill runtime");
293
- }
294
- return new OpenAIClient({
295
- apiKey: config.apiKey,
296
- model: config.model ?? DEFAULT_OPENAI_MODEL,
297
- maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS
298
- });
299
- }
300
- function createLlmClient(config) {
301
- if (config.provider === "openai") {
302
- return createOpenAIClient(config);
303
- }
304
- return createAnthropicClient(config);
305
- }
9
+ // src/skills/scriptSkill.ts
306
10
  var MAX_SCRIPT_OUTPUT = 1e6;
307
11
  var DEFAULT_SCRIPT_TIMEOUT_MS = 6e4;
308
12
  function runScript(cmd, args, opts) {
@@ -358,6 +62,7 @@ var ScriptSkill = class {
358
62
  priceSubunits;
359
63
  asset;
360
64
  mode = "llm";
65
+ llmOverride;
361
66
  image;
362
67
  imageFile;
363
68
  skillDir;
@@ -371,6 +76,7 @@ var ScriptSkill = class {
371
76
  this.capabilities = params.capabilities;
372
77
  this.priceSubunits = params.priceSubunits;
373
78
  this.asset = params.asset;
79
+ this.llmOverride = params.llmOverride;
374
80
  this.image = params.image;
375
81
  this.imageFile = params.imageFile;
376
82
  this.skillDir = params.skillDir;
@@ -380,11 +86,9 @@ var ScriptSkill = class {
380
86
  this.logger = params.logger ?? {};
381
87
  }
382
88
  async execute(input, ctx) {
383
- if (!ctx.llm) {
384
- throw new Error("LLM client not configured for skill runtime");
385
- }
89
+ const llm = this.resolveLlmClient(ctx);
386
90
  if (this.tools.length === 0) {
387
- const result = await ctx.llm.complete(this.systemPrompt, input.data, ctx.signal);
91
+ const result = await llm.complete(this.systemPrompt, input.data, ctx.signal);
388
92
  return { data: result };
389
93
  }
390
94
  const toolDefs = this.tools.map((tool) => ({
@@ -397,7 +101,6 @@ var ScriptSkill = class {
397
101
  }))
398
102
  }));
399
103
  const messages = [{ role: "user", content: input.data }];
400
- const llm = ctx.llm;
401
104
  for (let round = 0; round < this.maxToolRounds; round++) {
402
105
  if (ctx.signal?.aborted) {
403
106
  throw new Error("Job aborted");
@@ -429,6 +132,34 @@ var ScriptSkill = class {
429
132
  }
430
133
  throw new Error(`Max tool rounds (${this.maxToolRounds}) exceeded`);
431
134
  }
135
+ /**
136
+ * Resolve the LLM client for this skill from the runtime context.
137
+ *
138
+ * Contract:
139
+ * - When `llmOverride` is set, `ctx.getLlm` MUST be wired. Falling back to
140
+ * `ctx.llm` (the agent default) would silently use the wrong configuration
141
+ * for max-tokens-only overrides.
142
+ * - When no override is set, prefer `ctx.getLlm()` (returns the agent
143
+ * default), then fall back to `ctx.llm` for legacy callers that wire only
144
+ * a single client.
145
+ */
146
+ resolveLlmClient(ctx) {
147
+ let client;
148
+ if (this.llmOverride) {
149
+ client = ctx.getLlm?.(this.llmOverride);
150
+ if (!client) {
151
+ throw new Error(
152
+ `Skill "${this.name}" requires ctx.getLlm to be configured (llmOverride is set)`
153
+ );
154
+ }
155
+ return client;
156
+ }
157
+ client = ctx.getLlm?.() ?? ctx.llm;
158
+ if (!client) {
159
+ throw new Error("LLM client not configured for skill runtime");
160
+ }
161
+ return client;
162
+ }
432
163
  async runTool(toolDef, call, signal) {
433
164
  const args = [...toolDef.command];
434
165
  const cmd = args.shift();
@@ -654,6 +385,7 @@ function parseAssetAmount(asset, human) {
654
385
  Decimal.clone({ toExpNeg: -100, toExpPos: 100, precision: 50 });
655
386
 
656
387
  // src/skills/loader.ts
388
+ var MAX_TOKENS_LIMIT = 2e5;
657
389
  var DEFAULT_MAX_TOOL_ROUNDS = 10;
658
390
  var VALID_MODES = [
659
391
  "llm",
@@ -805,6 +537,44 @@ function validateScriptArgs(skillName, raw) {
805
537
  }
806
538
  return raw;
807
539
  }
540
+ function validateLlmOverride(skillName, frontmatter, mode) {
541
+ const hasProvider = frontmatter.provider !== void 0 && frontmatter.provider !== null;
542
+ const hasModel = frontmatter.model !== void 0 && frontmatter.model !== null;
543
+ const hasMaxTokens = frontmatter.max_tokens !== void 0 && frontmatter.max_tokens !== null;
544
+ if (!hasProvider && !hasModel && !hasMaxTokens) {
545
+ return void 0;
546
+ }
547
+ if (mode !== "llm") {
548
+ throw new Error(
549
+ `SKILL.md "${skillName}": "provider"/"model"/"max_tokens" are only valid in mode 'llm' (got '${mode}')`
550
+ );
551
+ }
552
+ if (hasProvider !== hasModel) {
553
+ throw new Error(
554
+ `SKILL.md "${skillName}": "provider" and "model" must be set together (declare both, or neither)`
555
+ );
556
+ }
557
+ const override = {};
558
+ if (hasProvider && hasModel) {
559
+ if (typeof frontmatter.provider !== "string" || frontmatter.provider.length === 0) {
560
+ throw new Error(`SKILL.md "${skillName}": "provider" must be a non-empty string`);
561
+ }
562
+ if (typeof frontmatter.model !== "string" || frontmatter.model.length === 0) {
563
+ throw new Error(`SKILL.md "${skillName}": "model" must be a non-empty string`);
564
+ }
565
+ override.provider = frontmatter.provider;
566
+ override.model = frontmatter.model;
567
+ }
568
+ if (hasMaxTokens) {
569
+ if (typeof frontmatter.max_tokens !== "number" || !Number.isInteger(frontmatter.max_tokens) || frontmatter.max_tokens <= 0 || frontmatter.max_tokens > MAX_TOKENS_LIMIT) {
570
+ throw new Error(
571
+ `SKILL.md "${skillName}": "max_tokens" must be a positive integer <= ${MAX_TOKENS_LIMIT}`
572
+ );
573
+ }
574
+ override.maxTokens = frontmatter.max_tokens;
575
+ }
576
+ return override;
577
+ }
808
578
  function validateScriptTimeoutMs(skillName, raw) {
809
579
  if (raw === void 0 || raw === null) {
810
580
  return void 0;
@@ -946,6 +716,7 @@ function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
946
716
  }
947
717
  const image = typeof frontmatter.image === "string" ? frontmatter.image : void 0;
948
718
  const imageFile = typeof frontmatter.image_file === "string" ? frontmatter.image_file : void 0;
719
+ const llmOverride = validateLlmOverride(frontmatter.name, frontmatter, mode);
949
720
  return {
950
721
  name: frontmatter.name,
951
722
  description: frontmatter.description,
@@ -956,6 +727,7 @@ function validateSkillFrontmatter(frontmatter, systemPrompt, options = {}) {
956
727
  systemPrompt,
957
728
  tools,
958
729
  maxToolRounds,
730
+ llmOverride,
959
731
  image,
960
732
  imageFile,
961
733
  outputFile,
@@ -977,6 +749,7 @@ function buildSkillFromParsed(parsed, skillDir, logger) {
977
749
  systemPrompt: parsed.systemPrompt,
978
750
  tools: parsed.tools,
979
751
  maxToolRounds: parsed.maxToolRounds,
752
+ llmOverride: parsed.llmOverride,
980
753
  image: parsed.image,
981
754
  imageFile: parsed.imageFile,
982
755
  logger
@@ -1064,6 +837,6 @@ function loadSkillsFromDir(skillsDir, options = {}) {
1064
837
  return skills;
1065
838
  }
1066
839
 
1067
- export { DEFAULT_MAX_TOOL_ROUNDS, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill, MAX_SCRIPT_OUTPUT, MAX_STATIC_FILE_SIZE, ScriptSkill, StaticFileSkill, StaticScriptSkill, createAnthropicClient, createLlmClient, createOpenAIClient, loadSkillsFromDir, parseSkillMd, resolveInsidePath, runScript, validateSkillFrontmatter };
840
+ export { DEFAULT_MAX_TOOL_ROUNDS, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill, MAX_SCRIPT_OUTPUT, MAX_STATIC_FILE_SIZE, ScriptSkill, StaticFileSkill, StaticScriptSkill, loadSkillsFromDir, parseSkillMd, resolveInsidePath, runScript, validateSkillFrontmatter };
1068
841
  //# sourceMappingURL=skills.js.map
1069
842
  //# sourceMappingURL=skills.js.map