promptopskit 0.3.9 → 0.4.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.
Files changed (70) hide show
  1. package/README.md +91 -7
  2. package/SKILL.md +69 -6
  3. package/dist/{chunk-YNIAA4ZJ.js → chunk-F6UJ73Y6.js} +5 -3
  4. package/dist/chunk-F6UJ73Y6.js.map +1 -0
  5. package/dist/{chunk-O6RQZYS4.js → chunk-HLNVKKMS.js} +16 -5
  6. package/dist/chunk-HLNVKKMS.js.map +1 -0
  7. package/dist/{chunk-4LEPCCSA.js → chunk-JBLUPKY4.js} +144 -3
  8. package/dist/chunk-JBLUPKY4.js.map +1 -0
  9. package/dist/{chunk-LMURMZUA.js → chunk-L2V7LDKD.js} +5 -3
  10. package/dist/chunk-L2V7LDKD.js.map +1 -0
  11. package/dist/{chunk-AN3OR7YC.js → chunk-RY5DVTLV.js} +7 -5
  12. package/dist/chunk-RY5DVTLV.js.map +1 -0
  13. package/dist/chunk-UPTCSWK3.js +60 -0
  14. package/dist/chunk-UPTCSWK3.js.map +1 -0
  15. package/dist/{chunk-V7QY7MRM.js → chunk-XNNDGDPI.js} +50 -3
  16. package/dist/chunk-XNNDGDPI.js.map +1 -0
  17. package/dist/cli/index.js +356 -5
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/index.cjs +398 -16
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +4 -4
  22. package/dist/index.d.ts +4 -4
  23. package/dist/index.js +162 -10
  24. package/dist/index.js.map +1 -1
  25. package/dist/providers/anthropic.cjs +200 -5
  26. package/dist/providers/anthropic.cjs.map +1 -1
  27. package/dist/providers/anthropic.d.cts +2 -2
  28. package/dist/providers/anthropic.d.ts +2 -2
  29. package/dist/providers/anthropic.js +3 -3
  30. package/dist/providers/gemini.cjs +191 -5
  31. package/dist/providers/gemini.cjs.map +1 -1
  32. package/dist/providers/gemini.d.cts +2 -2
  33. package/dist/providers/gemini.d.ts +2 -2
  34. package/dist/providers/gemini.js +3 -3
  35. package/dist/providers/openai-responses.cjs +189 -3
  36. package/dist/providers/openai-responses.cjs.map +1 -1
  37. package/dist/providers/openai-responses.d.cts +2 -2
  38. package/dist/providers/openai-responses.d.ts +2 -2
  39. package/dist/providers/openai-responses.js +3 -3
  40. package/dist/providers/openai.cjs +189 -3
  41. package/dist/providers/openai.cjs.map +1 -1
  42. package/dist/providers/openai.d.cts +2 -2
  43. package/dist/providers/openai.d.ts +2 -2
  44. package/dist/providers/openai.js +3 -3
  45. package/dist/providers/openrouter.cjs +224 -6
  46. package/dist/providers/openrouter.cjs.map +1 -1
  47. package/dist/providers/openrouter.d.cts +2 -2
  48. package/dist/providers/openrouter.d.ts +2 -2
  49. package/dist/providers/openrouter.js +4 -4
  50. package/dist/{schema-BKkg93rL.d.cts → schema-B9A1zLRk.d.cts} +482 -2
  51. package/dist/{schema-BKkg93rL.d.ts → schema-B9A1zLRk.d.ts} +482 -2
  52. package/dist/testing.cjs +217 -4
  53. package/dist/testing.cjs.map +1 -1
  54. package/dist/testing.d.cts +59 -2
  55. package/dist/testing.d.ts +59 -2
  56. package/dist/testing.js +70 -2
  57. package/dist/testing.js.map +1 -1
  58. package/dist/{types-CsQ8tON_.d.ts → types-DjGAwJxC.d.ts} +1 -1
  59. package/dist/{types-7cC17GWp.d.cts → types-DpbzzZO2.d.cts} +1 -1
  60. package/dist/usagetap/index.d.cts +2 -2
  61. package/dist/usagetap/index.d.ts +2 -2
  62. package/package.json +2 -2
  63. package/dist/chunk-4LEPCCSA.js.map +0 -1
  64. package/dist/chunk-AN3OR7YC.js.map +0 -1
  65. package/dist/chunk-LMURMZUA.js.map +0 -1
  66. package/dist/chunk-O6RQZYS4.js.map +0 -1
  67. package/dist/chunk-V7QY7MRM.js.map +0 -1
  68. package/dist/chunk-YAD6COQX.js +0 -26
  69. package/dist/chunk-YAD6COQX.js.map +0 -1
  70. package/dist/chunk-YNIAA4ZJ.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -145,6 +145,29 @@ function mergeOverride(base, override) {
145
145
  if (override.response !== void 0) {
146
146
  result.response = { ...result.response, ...override.response };
147
147
  }
148
+ if (override.cache !== void 0) {
149
+ result.cache = {
150
+ ...result.cache,
151
+ ...override.cache,
152
+ openai: mergeRecordBlock(result.cache?.openai, override.cache.openai),
153
+ anthropic: mergeRecordBlock(result.cache?.anthropic, override.cache.anthropic),
154
+ gemini: mergeRecordBlock(result.cache?.gemini, override.cache.gemini),
155
+ google: mergeRecordBlock(result.cache?.google, override.cache.google)
156
+ };
157
+ }
158
+ if (override.raw !== void 0) {
159
+ result.raw = {
160
+ ...result.raw,
161
+ ...override.raw,
162
+ openai: mergeRecordBlock(result.raw?.openai, override.raw.openai),
163
+ "openai-responses": mergeRecordBlock(result.raw?.["openai-responses"], override.raw["openai-responses"]),
164
+ openai_responses: mergeRecordBlock(result.raw?.openai_responses, override.raw.openai_responses),
165
+ anthropic: mergeRecordBlock(result.raw?.anthropic, override.raw.anthropic),
166
+ gemini: mergeRecordBlock(result.raw?.gemini, override.raw.gemini),
167
+ google: mergeRecordBlock(result.raw?.google, override.raw.google),
168
+ openrouter: mergeRecordBlock(result.raw?.openrouter, override.raw.openrouter)
169
+ };
170
+ }
148
171
  if (override.provider_options !== void 0) {
149
172
  result.provider_options = {
150
173
  ...result.provider_options,
@@ -156,11 +179,19 @@ function mergeOverride(base, override) {
156
179
  gemini: {
157
180
  ...result.provider_options?.gemini,
158
181
  ...override.provider_options.gemini
182
+ },
183
+ openrouter: {
184
+ ...result.provider_options?.openrouter,
185
+ ...override.provider_options.openrouter
159
186
  }
160
187
  };
161
188
  }
162
189
  return result;
163
190
  }
191
+ function mergeRecordBlock(base, override) {
192
+ if (override === void 0) return base;
193
+ return { ...base, ...override };
194
+ }
164
195
 
165
196
  // src/providers/resolve-asset.ts
166
197
  function resolveAssetForProvider(asset, runtime = {}) {
@@ -264,23 +295,42 @@ var ResponseSchema = import_zod.z.object({
264
295
  stream: import_zod.z.boolean().optional(),
265
296
  schema: import_zod.z.record(import_zod.z.unknown()).optional(),
266
297
  schema_name: import_zod.z.string().optional(),
298
+ schema_description: import_zod.z.string().optional(),
267
299
  schema_strict: import_zod.z.boolean().optional()
268
300
  });
269
301
  var AnthropicProviderOptionsSchema = import_zod.z.object({
270
302
  top_k: import_zod.z.number().int().min(0).optional(),
271
- tool_choice: import_zod.z.record(import_zod.z.unknown()).optional()
303
+ tool_choice: import_zod.z.record(import_zod.z.unknown()).optional(),
304
+ output_config: import_zod.z.record(import_zod.z.unknown()).optional()
272
305
  });
273
306
  var GeminiProviderOptionsSchema = import_zod.z.object({
274
307
  candidate_count: import_zod.z.number().int().positive().optional(),
275
308
  top_k: import_zod.z.number().int().min(0).optional(),
276
309
  seed: import_zod.z.number().int().optional(),
277
310
  response_schema: import_zod.z.record(import_zod.z.unknown()).optional(),
311
+ response_json_schema: import_zod.z.record(import_zod.z.unknown()).optional(),
278
312
  response_modalities: import_zod.z.array(import_zod.z.string()).optional(),
279
313
  thinking_budget_tokens: import_zod.z.number().int().positive().optional()
280
314
  });
315
+ var OpenRouterProviderOptionsSchema = import_zod.z.object({
316
+ provider: import_zod.z.record(import_zod.z.unknown()).optional(),
317
+ transforms: import_zod.z.array(import_zod.z.string()).optional(),
318
+ plugins: import_zod.z.array(import_zod.z.record(import_zod.z.unknown())).optional(),
319
+ models: import_zod.z.array(import_zod.z.string()).optional()
320
+ });
281
321
  var ProviderOptionsSchema = import_zod.z.object({
282
322
  anthropic: AnthropicProviderOptionsSchema.optional(),
283
- gemini: GeminiProviderOptionsSchema.optional()
323
+ gemini: GeminiProviderOptionsSchema.optional(),
324
+ openrouter: OpenRouterProviderOptionsSchema.optional()
325
+ });
326
+ var RawProviderBodySchema = import_zod.z.object({
327
+ openai: import_zod.z.record(import_zod.z.unknown()).optional(),
328
+ "openai-responses": import_zod.z.record(import_zod.z.unknown()).optional(),
329
+ openai_responses: import_zod.z.record(import_zod.z.unknown()).optional(),
330
+ anthropic: import_zod.z.record(import_zod.z.unknown()).optional(),
331
+ gemini: import_zod.z.record(import_zod.z.unknown()).optional(),
332
+ google: import_zod.z.record(import_zod.z.unknown()).optional(),
333
+ openrouter: import_zod.z.record(import_zod.z.unknown()).optional()
284
334
  });
285
335
  var OpenAICacheSchema = import_zod.z.object({
286
336
  prompt_cache_key: import_zod.z.string().min(1).optional(),
@@ -353,6 +403,7 @@ var PromptAssetOverridesSchema = import_zod.z.object({
353
403
  sampling: SamplingSchema.optional(),
354
404
  response: ResponseSchema.optional(),
355
405
  cache: CacheSchema.optional(),
406
+ raw: RawProviderBodySchema.optional(),
356
407
  tools: import_zod.z.array(ToolRefSchema).optional(),
357
408
  provider_options: ProviderOptionsSchema.optional()
358
409
  });
@@ -385,6 +436,7 @@ var PromptAssetSchema = import_zod.z.object({
385
436
  sampling: SamplingSchema.optional(),
386
437
  response: ResponseSchema.optional(),
387
438
  cache: CacheSchema.optional(),
439
+ raw: RawProviderBodySchema.optional(),
388
440
  tools: import_zod.z.array(ToolRefSchema).optional(),
389
441
  provider_options: ProviderOptionsSchema.optional(),
390
442
  mcp: MCPSchema.optional(),
@@ -398,6 +450,123 @@ var PromptAssetSchema = import_zod.z.object({
398
450
  source: SourceSchema.optional()
399
451
  });
400
452
 
453
+ // src/parser/frontmatter-guard.ts
454
+ var FRONT_MATTER_DELIMITER = /^---[ \t]*$/;
455
+ var REGEX_FIELD = /^(?<indent>\s*)(?<field>allow_regex|deny_regex)\s*:\s*(?<value>.*)$/;
456
+ var PATTERN_FIELD = /^(?<indent>\s*)pattern\s*:\s*(?<value>.*)$/;
457
+ var INLINE_PATTERN_FIELD = /(?:^|[{,]\s*)pattern\s*:\s*(?<value>"(?:\\.|[^"\\])*")/;
458
+ function assertRegexFrontMatterQuoting(content, filePath) {
459
+ const frontMatter = extractFrontMatter(content);
460
+ if (!frontMatter) {
461
+ return;
462
+ }
463
+ let pendingRegex;
464
+ for (const [index, line] of frontMatter.lines.entries()) {
465
+ const lineNumber = frontMatter.startLine + index;
466
+ const regexMatch = line.match(REGEX_FIELD);
467
+ if (regexMatch?.groups) {
468
+ const field = regexMatch.groups.field;
469
+ const indent2 = regexMatch.groups.indent.length;
470
+ const value = regexMatch.groups.value.trim();
471
+ pendingRegex = value === "" ? { field, indent: indent2 } : void 0;
472
+ assertNoUnescapedBackslashInDoubleQuotedRegex(value, field, filePath, lineNumber);
473
+ assertNoUnescapedBackslashInInlinePattern(value, field, filePath, lineNumber);
474
+ continue;
475
+ }
476
+ if (!pendingRegex) {
477
+ continue;
478
+ }
479
+ if (line.trim() === "" || line.trimStart().startsWith("#")) {
480
+ continue;
481
+ }
482
+ const indent = line.match(/^\s*/)?.[0].length ?? 0;
483
+ if (indent <= pendingRegex.indent) {
484
+ pendingRegex = void 0;
485
+ continue;
486
+ }
487
+ const patternMatch = line.match(PATTERN_FIELD);
488
+ if (!patternMatch?.groups) {
489
+ continue;
490
+ }
491
+ assertNoUnescapedBackslashInDoubleQuotedRegex(
492
+ patternMatch.groups.value.trim(),
493
+ `${pendingRegex.field}.pattern`,
494
+ filePath,
495
+ lineNumber
496
+ );
497
+ }
498
+ }
499
+ function assertNoUnescapedBackslashInInlinePattern(value, field, filePath, lineNumber) {
500
+ const match = value.match(INLINE_PATTERN_FIELD);
501
+ const pattern = match?.groups?.value;
502
+ if (!pattern) {
503
+ return;
504
+ }
505
+ assertNoUnescapedBackslashInDoubleQuotedRegex(pattern, `${field}.pattern`, filePath, lineNumber);
506
+ }
507
+ function extractFrontMatter(content) {
508
+ const lines = content.split(/\r?\n/);
509
+ if (!FRONT_MATTER_DELIMITER.test(lines[0] ?? "")) {
510
+ return void 0;
511
+ }
512
+ for (let index = 1; index < lines.length; index += 1) {
513
+ if (FRONT_MATTER_DELIMITER.test(lines[index] ?? "")) {
514
+ return {
515
+ lines: lines.slice(1, index),
516
+ startLine: 2
517
+ };
518
+ }
519
+ }
520
+ return void 0;
521
+ }
522
+ function assertNoUnescapedBackslashInDoubleQuotedRegex(value, field, filePath, lineNumber) {
523
+ if (!value.startsWith('"')) {
524
+ return;
525
+ }
526
+ const quoted = readDoubleQuotedScalar(value);
527
+ if (!quoted) {
528
+ return;
529
+ }
530
+ if (!hasUnescapedBackslash(quoted)) {
531
+ return;
532
+ }
533
+ const location = filePath ? `${filePath}:${lineNumber}` : `line ${lineNumber}`;
534
+ throw new Error(
535
+ `POK013: Invalid context regex YAML at ${location}, field "${field}": double-quoted regex strings treat backslashes as YAML escapes. Use unquoted /pattern/i literal form, single quotes, or double each backslash.`
536
+ );
537
+ }
538
+ function readDoubleQuotedScalar(value) {
539
+ let result = "";
540
+ for (let index = 1; index < value.length; index += 1) {
541
+ const char = value[index];
542
+ if (char === '"') {
543
+ return result;
544
+ }
545
+ result += char;
546
+ if (char === "\\" && index + 1 < value.length) {
547
+ index += 1;
548
+ result += value[index];
549
+ }
550
+ }
551
+ return void 0;
552
+ }
553
+ function hasUnescapedBackslash(value) {
554
+ for (let index = 0; index < value.length; index += 1) {
555
+ if (value[index] !== "\\") {
556
+ continue;
557
+ }
558
+ let count = 1;
559
+ while (value[index + count] === "\\") {
560
+ count += 1;
561
+ }
562
+ if (count % 2 === 1) {
563
+ return true;
564
+ }
565
+ index += count - 1;
566
+ }
567
+ return false;
568
+ }
569
+
401
570
  // src/parser/sections.ts
402
571
  var SECTION_MAP = {
403
572
  "system instructions": "system_instructions",
@@ -438,6 +607,7 @@ function extractSections(body) {
438
607
 
439
608
  // src/parser/parser.ts
440
609
  function parsePrompt(content, filePath) {
610
+ assertRegexFrontMatterQuoting(content, filePath);
441
611
  const { data: frontMatter, content: body } = (0, import_gray_matter.default)(content);
442
612
  const sections = extractSections(body);
443
613
  const raw = {
@@ -1018,6 +1188,21 @@ function withPromptInputSupport(adapter) {
1018
1188
  };
1019
1189
  }
1020
1190
 
1191
+ // src/providers/raw.ts
1192
+ function applyRawProviderBody(body, asset, provider) {
1193
+ const raw = getRawProviderBody(asset, provider);
1194
+ return raw ? { ...body, ...raw } : body;
1195
+ }
1196
+ function getRawProviderBody(asset, provider) {
1197
+ if (provider === "openai-responses") {
1198
+ return asset.raw?.["openai-responses"] ?? asset.raw?.openai_responses;
1199
+ }
1200
+ if (provider === "gemini") {
1201
+ return asset.raw?.gemini ?? asset.raw?.google;
1202
+ }
1203
+ return asset.raw?.[provider];
1204
+ }
1205
+
1021
1206
  // src/providers/openai.ts
1022
1207
  var openaiAdapter = withPromptInputSupport({
1023
1208
  name: "openai",
@@ -1073,6 +1258,7 @@ var openaiAdapter = withPromptInputSupport({
1073
1258
  type: "json_schema",
1074
1259
  json_schema: {
1075
1260
  name: resolvedAsset.response.schema_name ?? `${resolvedAsset.id}_response`,
1261
+ ...resolvedAsset.response.schema_description ? { description: resolvedAsset.response.schema_description } : {},
1076
1262
  schema: resolvedAsset.response.schema,
1077
1263
  strict: resolvedAsset.response.schema_strict ?? true
1078
1264
  }
@@ -1107,7 +1293,7 @@ var openaiAdapter = withPromptInputSupport({
1107
1293
  });
1108
1294
  }
1109
1295
  return {
1110
- body,
1296
+ body: applyRawProviderBody(body, resolvedAsset, "openai"),
1111
1297
  provider: "openai",
1112
1298
  model: resolvedAsset.model ?? "unknown"
1113
1299
  };
@@ -1174,6 +1360,7 @@ var openaiResponsesAdapter = withPromptInputSupport({
1174
1360
  format: {
1175
1361
  type: "json_schema",
1176
1362
  name: resolvedAsset.response.schema_name ?? `${resolvedAsset.id}_response`,
1363
+ ...resolvedAsset.response.schema_description ? { description: resolvedAsset.response.schema_description } : {},
1177
1364
  schema: resolvedAsset.response.schema,
1178
1365
  strict: resolvedAsset.response.schema_strict ?? true
1179
1366
  }
@@ -1208,7 +1395,7 @@ var openaiResponsesAdapter = withPromptInputSupport({
1208
1395
  if (responseOptions?.include !== void 0) body.include = responseOptions.include;
1209
1396
  if (responseOptions?.background !== void 0) body.background = responseOptions.background;
1210
1397
  return {
1211
- body,
1398
+ body: applyRawProviderBody(body, resolvedAsset, "openai-responses"),
1212
1399
  provider: "openai-responses",
1213
1400
  model: resolvedAsset.model ?? "unknown"
1214
1401
  };
@@ -1234,8 +1421,8 @@ var anthropicAdapter = withPromptInputSupport({
1234
1421
  if (resolvedAsset.reasoning?.effort !== void 0) {
1235
1422
  warnings.push("Anthropic uses budget_tokens for thinking, not effort. effort will be mapped approximately.");
1236
1423
  }
1237
- if (resolvedAsset.response?.schema !== void 0) {
1238
- warnings.push("Anthropic does not support response.schema structured output in this adapter. It will be ignored.");
1424
+ if (resolvedAsset.response?.schema !== void 0 && resolvedAsset.response?.format !== "json") {
1425
+ warnings.push("Anthropic response.schema is mapped to output_config.format and should usually be paired with response.format: json.");
1239
1426
  }
1240
1427
  if (resolvedAsset.provider_options?.anthropic?.top_k !== void 0 && resolvedAsset.provider_options.anthropic.top_k < 0) {
1241
1428
  errors.push("Anthropic provider_options.top_k must be >= 0.");
@@ -1299,6 +1486,16 @@ var anthropicAdapter = withPromptInputSupport({
1299
1486
  if (resolvedAsset.provider_options?.anthropic?.top_k !== void 0) {
1300
1487
  body.top_k = resolvedAsset.provider_options.anthropic.top_k;
1301
1488
  }
1489
+ if (resolvedAsset.provider_options?.anthropic?.output_config !== void 0) {
1490
+ body.output_config = resolvedAsset.provider_options.anthropic.output_config;
1491
+ } else if (resolvedAsset.response?.schema !== void 0) {
1492
+ body.output_config = {
1493
+ format: {
1494
+ type: "json_schema",
1495
+ schema: resolvedAsset.response.schema
1496
+ }
1497
+ };
1498
+ }
1302
1499
  if (resolvedAsset.response?.stream !== void 0) {
1303
1500
  body.stream = resolvedAsset.response.stream;
1304
1501
  }
@@ -1332,7 +1529,7 @@ var anthropicAdapter = withPromptInputSupport({
1332
1529
  body.tool_choice = resolvedAsset.provider_options.anthropic.tool_choice;
1333
1530
  }
1334
1531
  return {
1335
- body,
1532
+ body: applyRawProviderBody(body, resolvedAsset, "anthropic"),
1336
1533
  provider: "anthropic",
1337
1534
  model: resolvedAsset.model ?? "unknown"
1338
1535
  };
@@ -1404,9 +1601,10 @@ var geminiAdapter = withPromptInputSupport({
1404
1601
  if (geminiOptions?.candidate_count !== void 0) generationConfig.candidateCount = geminiOptions.candidate_count;
1405
1602
  if (geminiOptions?.top_k !== void 0) generationConfig.topK = geminiOptions.top_k;
1406
1603
  if (geminiOptions?.seed !== void 0) generationConfig.seed = geminiOptions.seed;
1407
- if (geminiOptions?.response_schema !== void 0) generationConfig.responseSchema = geminiOptions.response_schema;
1408
1604
  if (geminiOptions?.response_modalities !== void 0) generationConfig.responseModalities = geminiOptions.response_modalities;
1409
- if (resolvedAsset.response?.schema !== void 0) generationConfig.responseSchema = resolvedAsset.response.schema;
1605
+ if (resolvedAsset.response?.schema !== void 0) generationConfig.responseJsonSchema = resolvedAsset.response.schema;
1606
+ if (geminiOptions?.response_schema !== void 0) generationConfig.responseSchema = geminiOptions.response_schema;
1607
+ if (geminiOptions?.response_json_schema !== void 0) generationConfig.responseJsonSchema = geminiOptions.response_json_schema;
1410
1608
  if (resolvedAsset.response?.format === "json") {
1411
1609
  generationConfig.responseMimeType = "application/json";
1412
1610
  }
@@ -1441,7 +1639,7 @@ var geminiAdapter = withPromptInputSupport({
1441
1639
  body.tools = [{ functionDeclarations }];
1442
1640
  }
1443
1641
  return {
1444
- body,
1642
+ body: applyRawProviderBody(body, resolvedAsset, "gemini"),
1445
1643
  provider: "gemini",
1446
1644
  model: resolvedAsset.model ?? "unknown"
1447
1645
  };
@@ -1452,13 +1650,45 @@ var geminiAdapter = withPromptInputSupport({
1452
1650
  var openrouterAdapter = withPromptInputSupport({
1453
1651
  name: "openrouter",
1454
1652
  validate(asset, runtime) {
1455
- return openaiAdapter.validate(asset, runtime);
1653
+ const resolvedAsset = resolveAssetForProvider(asset, runtime);
1654
+ const validation = openaiAdapter.validate(asset, runtime);
1655
+ const hasOpenRouterModels = (resolvedAsset.provider_options?.openrouter?.models?.length ?? 0) > 0;
1656
+ if (!hasOpenRouterModels) {
1657
+ return validation;
1658
+ }
1659
+ const modelRequiredError = "OpenAI adapter requires a model to be specified.";
1660
+ const errors = validation.errors.filter((error) => error !== modelRequiredError);
1661
+ const warnings = validation.warnings.filter((warning) => warning !== modelRequiredError);
1662
+ return {
1663
+ valid: errors.length === 0,
1664
+ errors,
1665
+ warnings
1666
+ };
1456
1667
  },
1457
1668
  render(asset, runtime) {
1458
- const result = openaiAdapter.render(asset, runtime);
1669
+ const resolvedAsset = resolveAssetForProvider(asset, runtime);
1670
+ const result = openaiAdapter.render(
1671
+ { ...resolvedAsset, raw: void 0 },
1672
+ {
1673
+ variables: runtime.variables,
1674
+ history: runtime.history,
1675
+ toolRegistry: runtime.toolRegistry,
1676
+ strict: runtime.strict
1677
+ }
1678
+ );
1679
+ const openrouterOptions = resolvedAsset.provider_options?.openrouter;
1680
+ let body = { ...result.body };
1681
+ if (openrouterOptions?.provider !== void 0) body.provider = openrouterOptions.provider;
1682
+ if (openrouterOptions?.transforms !== void 0) body.transforms = openrouterOptions.transforms;
1683
+ if (openrouterOptions?.plugins !== void 0) body.plugins = openrouterOptions.plugins;
1684
+ if (openrouterOptions?.models !== void 0) body.models = openrouterOptions.models;
1685
+ if (body.model === void 0) delete body.model;
1686
+ body = applyRawProviderBody(body, resolvedAsset, "openrouter");
1459
1687
  return {
1460
1688
  ...result,
1461
- provider: "openrouter"
1689
+ body,
1690
+ provider: "openrouter",
1691
+ model: resolvedAsset.model ?? openrouterOptions?.models?.[0] ?? "unknown"
1462
1692
  };
1463
1693
  }
1464
1694
  });
@@ -1522,8 +1752,21 @@ var KNOWN_FRONT_MATTER_KEYS = /* @__PURE__ */ new Set([
1522
1752
  "environments",
1523
1753
  "tiers",
1524
1754
  "metadata",
1525
- "cache"
1755
+ "cache",
1756
+ "provider_options",
1757
+ "raw"
1526
1758
  ]);
1759
+ var RISKY_UNBOUNDED_INPUT_NAMES = [
1760
+ "message",
1761
+ "prompt",
1762
+ "history",
1763
+ "transcript",
1764
+ "document",
1765
+ "content",
1766
+ "input",
1767
+ "body",
1768
+ "context"
1769
+ ];
1527
1770
  function validateAsset(asset, frontMatterKeys, filePath) {
1528
1771
  const errors = [];
1529
1772
  const warnings = [];
@@ -1594,7 +1837,32 @@ function validateAsset(asset, frontMatterKeys, filePath) {
1594
1837
  });
1595
1838
  }
1596
1839
  }
1840
+ if (usedVars.size > 0 && (!asset.context?.inputs || asset.context.inputs.length === 0)) {
1841
+ warnings.push({
1842
+ code: "POK046",
1843
+ message: `Template uses ${usedVars.size === 1 ? "a variable" : "variables"} but context.inputs is not declared.`,
1844
+ filePath,
1845
+ suggestion: "Declare context.inputs to enable input policy validation."
1846
+ });
1847
+ }
1597
1848
  for (const input of getContextInputs(asset)) {
1849
+ const lowerName = input.name.toLowerCase();
1850
+ if (input.max_size === void 0 && RISKY_UNBOUNDED_INPUT_NAMES.some((needle) => lowerName.includes(needle))) {
1851
+ warnings.push({
1852
+ code: "POK040",
1853
+ message: `Context input "${input.name}" has no max_size and appears unbounded.`,
1854
+ filePath,
1855
+ suggestion: "Add max_size to constrain prompt payload growth."
1856
+ });
1857
+ }
1858
+ if (input.allow_regex === void 0 && input.deny_regex === void 0 && input.non_empty === void 0 && input.reject_secrets === void 0) {
1859
+ warnings.push({
1860
+ code: "POK041",
1861
+ message: `Context input "${input.name}" has no input hardening validators.`,
1862
+ filePath,
1863
+ suggestion: "Consider non_empty/reject_secrets and allow/deny regex validators."
1864
+ });
1865
+ }
1598
1866
  if (input.trim !== void 0 && input.trim !== false && input.max_size === void 0) {
1599
1867
  warnings.push({
1600
1868
  code: "POK014",
@@ -1622,6 +1890,91 @@ function validateAsset(asset, frontMatterKeys, filePath) {
1622
1890
  }
1623
1891
  }
1624
1892
  }
1893
+ if (asset.provider) {
1894
+ let providerCache;
1895
+ let cacheSuggestionField;
1896
+ switch (asset.provider) {
1897
+ case "openai":
1898
+ providerCache = asset.cache?.openai;
1899
+ cacheSuggestionField = "cache.openai";
1900
+ break;
1901
+ case "anthropic":
1902
+ providerCache = asset.cache?.anthropic;
1903
+ cacheSuggestionField = "cache.anthropic";
1904
+ break;
1905
+ case "gemini":
1906
+ case "google":
1907
+ providerCache = asset.cache?.gemini ?? asset.cache?.google;
1908
+ cacheSuggestionField = "cache.gemini";
1909
+ break;
1910
+ default:
1911
+ break;
1912
+ }
1913
+ if (cacheSuggestionField && providerCache === void 0) {
1914
+ warnings.push({
1915
+ code: "POK042",
1916
+ message: `Provider "${asset.provider}" has no provider-specific cache settings.`,
1917
+ filePath,
1918
+ suggestion: `Consider configuring ${cacheSuggestionField} for better cache-hit behavior.`
1919
+ });
1920
+ }
1921
+ if (!asset.model) {
1922
+ warnings.push({
1923
+ code: "POK044",
1924
+ message: `Provider "${asset.provider}" is configured without a model.`,
1925
+ filePath,
1926
+ suggestion: "Set model in prompt or defaults to avoid adapter-time errors."
1927
+ });
1928
+ }
1929
+ }
1930
+ if (asset.cache?.gemini?.cached_content && asset.cache.google?.cached_content && asset.cache.gemini.cached_content !== asset.cache.google.cached_content) {
1931
+ warnings.push({
1932
+ code: "POK043",
1933
+ message: "cache.gemini.cached_content and cache.google.cached_content are both set to different values.",
1934
+ filePath,
1935
+ suggestion: "Use one canonical value; Gemini prefers cache.gemini.cached_content."
1936
+ });
1937
+ }
1938
+ for (const [envName, overrides] of Object.entries(asset.environments ?? {})) {
1939
+ if (asset.cache && (!isRecord(overrides) || !overrides.cache)) {
1940
+ warnings.push({
1941
+ code: "POK045",
1942
+ message: `Environment "${envName}" does not override cache while prompt-level cache is defined.`,
1943
+ filePath,
1944
+ suggestion: "Confirm cache strategy is intentionally shared across environments."
1945
+ });
1946
+ }
1947
+ }
1948
+ for (const [tierName, overrides] of Object.entries(asset.tiers ?? {})) {
1949
+ if (asset.cache && (!isRecord(overrides) || !overrides.cache)) {
1950
+ warnings.push({
1951
+ code: "POK045",
1952
+ message: `Tier "${tierName}" does not override cache while prompt-level cache is defined.`,
1953
+ filePath,
1954
+ suggestion: "Confirm cache strategy is intentionally shared across tiers."
1955
+ });
1956
+ }
1957
+ }
1958
+ for (const tool of asset.tools ?? []) {
1959
+ if (isRecord(tool)) {
1960
+ if (!tool.description) {
1961
+ warnings.push({
1962
+ code: "POK047",
1963
+ message: `Inline tool "${tool.name}" is missing a description.`,
1964
+ filePath,
1965
+ suggestion: "Add description to improve model tool-selection quality."
1966
+ });
1967
+ }
1968
+ if (!tool.input_schema) {
1969
+ warnings.push({
1970
+ code: "POK047",
1971
+ message: `Inline tool "${tool.name}" is missing input_schema.`,
1972
+ filePath,
1973
+ suggestion: "Add input_schema so tool inputs are strongly typed."
1974
+ });
1975
+ }
1976
+ }
1977
+ }
1625
1978
  return {
1626
1979
  valid: errors.length === 0,
1627
1980
  errors,
@@ -1658,6 +2011,9 @@ function findClosestMatch(input, candidates) {
1658
2011
  }
1659
2012
  return best;
1660
2013
  }
2014
+ function isRecord(value) {
2015
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2016
+ }
1661
2017
 
1662
2018
  // src/usagetap/client.ts
1663
2019
  var DEFAULT_BASE_URL = "https://api.usagetap.com";
@@ -2141,9 +2497,21 @@ var PromptOpsKit = class {
2141
2497
  * Validate a prompt file.
2142
2498
  */
2143
2499
  async validatePrompt(promptPath) {
2144
- const asset = await this.loadPrompt(promptPath);
2145
2500
  const sourceFile = (0, import_node_path4.resolve)(this.config.sourceDir, promptPath + ".md");
2146
- return validateAssetWithIncludes(asset, sourceFile);
2501
+ try {
2502
+ const asset = await this.loadPrompt(promptPath);
2503
+ return validateAssetWithIncludes(asset, sourceFile);
2504
+ } catch (error) {
2505
+ const validationError = toPromptValidationError(error, sourceFile);
2506
+ if (validationError) {
2507
+ return {
2508
+ valid: false,
2509
+ errors: [validationError],
2510
+ warnings: []
2511
+ };
2512
+ }
2513
+ throw error;
2514
+ }
2147
2515
  }
2148
2516
  /**
2149
2517
  * Clear the internal cache.
@@ -2155,6 +2523,20 @@ var PromptOpsKit = class {
2155
2523
  function createPromptOpsKit(config = {}) {
2156
2524
  return new PromptOpsKit(config);
2157
2525
  }
2526
+ function toPromptValidationError(error, filePath) {
2527
+ if (!(error instanceof Error)) {
2528
+ return void 0;
2529
+ }
2530
+ const match = error.message.match(/^(POK\d+):\s*(.*)$/);
2531
+ if (!match) {
2532
+ return void 0;
2533
+ }
2534
+ return {
2535
+ code: match[1],
2536
+ message: match[2],
2537
+ filePath
2538
+ };
2539
+ }
2158
2540
  async function renderPrompt(options) {
2159
2541
  const kit = createPromptOpsKit({
2160
2542
  sourceDir: options.sourceDir ?? DEFAULT_PROMPTS_DIR,