@trazum/core 1.8.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 (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +163 -0
  3. package/dist/advisories.d.ts +23 -0
  4. package/dist/advisories.d.ts.map +1 -0
  5. package/dist/advisories.js +376 -0
  6. package/dist/advisories.js.map +1 -0
  7. package/dist/aws-sigv4.d.ts +88 -0
  8. package/dist/aws-sigv4.d.ts.map +1 -0
  9. package/dist/aws-sigv4.js +117 -0
  10. package/dist/aws-sigv4.js.map +1 -0
  11. package/dist/baseline.d.ts +171 -0
  12. package/dist/baseline.d.ts.map +1 -0
  13. package/dist/baseline.js +273 -0
  14. package/dist/baseline.js.map +1 -0
  15. package/dist/cache.d.ts +26 -0
  16. package/dist/cache.d.ts.map +1 -0
  17. package/dist/cache.js +28 -0
  18. package/dist/cache.js.map +1 -0
  19. package/dist/changes.d.ts +29 -0
  20. package/dist/changes.d.ts.map +1 -0
  21. package/dist/changes.js +142 -0
  22. package/dist/changes.js.map +1 -0
  23. package/dist/compare.d.ts +65 -0
  24. package/dist/compare.d.ts.map +1 -0
  25. package/dist/compare.js +58 -0
  26. package/dist/compare.js.map +1 -0
  27. package/dist/config-schema.d.ts +118 -0
  28. package/dist/config-schema.d.ts.map +1 -0
  29. package/dist/config-schema.js +315 -0
  30. package/dist/config-schema.js.map +1 -0
  31. package/dist/config.d.ts +47 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/config.js +112 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/detect.d.ts +70 -0
  36. package/dist/detect.d.ts.map +1 -0
  37. package/dist/detect.js +228 -0
  38. package/dist/detect.js.map +1 -0
  39. package/dist/evaluate.d.ts +98 -0
  40. package/dist/evaluate.d.ts.map +1 -0
  41. package/dist/evaluate.js +110 -0
  42. package/dist/evaluate.js.map +1 -0
  43. package/dist/extract.d.ts +81 -0
  44. package/dist/extract.d.ts.map +1 -0
  45. package/dist/extract.js +280 -0
  46. package/dist/extract.js.map +1 -0
  47. package/dist/gcp-auth.d.ts +58 -0
  48. package/dist/gcp-auth.d.ts.map +1 -0
  49. package/dist/gcp-auth.js +113 -0
  50. package/dist/gcp-auth.js.map +1 -0
  51. package/dist/glob.d.ts +49 -0
  52. package/dist/glob.d.ts.map +1 -0
  53. package/dist/glob.js +154 -0
  54. package/dist/glob.js.map +1 -0
  55. package/dist/host.d.ts +30 -0
  56. package/dist/host.d.ts.map +1 -0
  57. package/dist/host.js +69 -0
  58. package/dist/host.js.map +1 -0
  59. package/dist/i18n/en.d.ts +4 -0
  60. package/dist/i18n/en.d.ts.map +1 -0
  61. package/dist/i18n/en.js +168 -0
  62. package/dist/i18n/en.js.map +1 -0
  63. package/dist/i18n/es.d.ts +4 -0
  64. package/dist/i18n/es.d.ts.map +1 -0
  65. package/dist/i18n/es.js +168 -0
  66. package/dist/i18n/es.js.map +1 -0
  67. package/dist/i18n/index.d.ts +36 -0
  68. package/dist/i18n/index.d.ts.map +1 -0
  69. package/dist/i18n/index.js +50 -0
  70. package/dist/i18n/index.js.map +1 -0
  71. package/dist/i18n/types.d.ts +180 -0
  72. package/dist/i18n/types.d.ts.map +1 -0
  73. package/dist/i18n/types.js +11 -0
  74. package/dist/i18n/types.js.map +1 -0
  75. package/dist/index.d.ts +66 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +69 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/llm.d.ts +226 -0
  80. package/dist/llm.d.ts.map +1 -0
  81. package/dist/llm.js +485 -0
  82. package/dist/llm.js.map +1 -0
  83. package/dist/nearest.d.ts +20 -0
  84. package/dist/nearest.d.ts.map +1 -0
  85. package/dist/nearest.js +54 -0
  86. package/dist/nearest.js.map +1 -0
  87. package/dist/net.d.ts +90 -0
  88. package/dist/net.d.ts.map +1 -0
  89. package/dist/net.js +203 -0
  90. package/dist/net.js.map +1 -0
  91. package/dist/node.d.ts +32 -0
  92. package/dist/node.d.ts.map +1 -0
  93. package/dist/node.js +33 -0
  94. package/dist/node.js.map +1 -0
  95. package/dist/openrouter.d.ts +25 -0
  96. package/dist/openrouter.d.ts.map +1 -0
  97. package/dist/openrouter.js +72 -0
  98. package/dist/openrouter.js.map +1 -0
  99. package/dist/optimize.d.ts +38 -0
  100. package/dist/optimize.d.ts.map +1 -0
  101. package/dist/optimize.js +183 -0
  102. package/dist/optimize.js.map +1 -0
  103. package/dist/otlp.d.ts +91 -0
  104. package/dist/otlp.d.ts.map +1 -0
  105. package/dist/otlp.js +102 -0
  106. package/dist/otlp.js.map +1 -0
  107. package/dist/phrases.d.ts +169 -0
  108. package/dist/phrases.d.ts.map +1 -0
  109. package/dist/phrases.js +939 -0
  110. package/dist/phrases.js.map +1 -0
  111. package/dist/pricing-overlay.d.ts +55 -0
  112. package/dist/pricing-overlay.d.ts.map +1 -0
  113. package/dist/pricing-overlay.js +241 -0
  114. package/dist/pricing-overlay.js.map +1 -0
  115. package/dist/pricing.d.ts +115 -0
  116. package/dist/pricing.d.ts.map +1 -0
  117. package/dist/pricing.js +400 -0
  118. package/dist/pricing.js.map +1 -0
  119. package/dist/profile.d.ts +71 -0
  120. package/dist/profile.d.ts.map +1 -0
  121. package/dist/profile.js +55 -0
  122. package/dist/profile.js.map +1 -0
  123. package/dist/promptfoo.d.ts +58 -0
  124. package/dist/promptfoo.d.ts.map +1 -0
  125. package/dist/promptfoo.js +149 -0
  126. package/dist/promptfoo.js.map +1 -0
  127. package/dist/prune.d.ts +91 -0
  128. package/dist/prune.d.ts.map +1 -0
  129. package/dist/prune.js +110 -0
  130. package/dist/prune.js.map +1 -0
  131. package/dist/reorder.d.ts +82 -0
  132. package/dist/reorder.d.ts.map +1 -0
  133. package/dist/reorder.js +215 -0
  134. package/dist/reorder.js.map +1 -0
  135. package/dist/review.d.ts +54 -0
  136. package/dist/review.d.ts.map +1 -0
  137. package/dist/review.js +131 -0
  138. package/dist/review.js.map +1 -0
  139. package/dist/rules.d.ts +5 -0
  140. package/dist/rules.d.ts.map +1 -0
  141. package/dist/rules.js +279 -0
  142. package/dist/rules.js.map +1 -0
  143. package/dist/savings.d.ts +36 -0
  144. package/dist/savings.d.ts.map +1 -0
  145. package/dist/savings.js +83 -0
  146. package/dist/savings.js.map +1 -0
  147. package/dist/segment.d.ts +8 -0
  148. package/dist/segment.d.ts.map +1 -0
  149. package/dist/segment.js +74 -0
  150. package/dist/segment.js.map +1 -0
  151. package/dist/shared-prefix.d.ts +63 -0
  152. package/dist/shared-prefix.d.ts.map +1 -0
  153. package/dist/shared-prefix.js +151 -0
  154. package/dist/shared-prefix.js.map +1 -0
  155. package/dist/similarity.d.ts +13 -0
  156. package/dist/similarity.d.ts.map +1 -0
  157. package/dist/similarity.js +30 -0
  158. package/dist/similarity.js.map +1 -0
  159. package/dist/structure.d.ts +144 -0
  160. package/dist/structure.d.ts.map +1 -0
  161. package/dist/structure.js +455 -0
  162. package/dist/structure.js.map +1 -0
  163. package/dist/suggest.d.ts +100 -0
  164. package/dist/suggest.d.ts.map +1 -0
  165. package/dist/suggest.js +151 -0
  166. package/dist/suggest.js.map +1 -0
  167. package/dist/tokenizer.d.ts +57 -0
  168. package/dist/tokenizer.d.ts.map +1 -0
  169. package/dist/tokenizer.js +157 -0
  170. package/dist/tokenizer.js.map +1 -0
  171. package/dist/types.d.ts +296 -0
  172. package/dist/types.d.ts.map +1 -0
  173. package/dist/types.js +3 -0
  174. package/dist/types.js.map +1 -0
  175. package/dist/walk.d.ts +40 -0
  176. package/dist/walk.d.ts.map +1 -0
  177. package/dist/walk.js +95 -0
  178. package/dist/walk.js.map +1 -0
  179. package/package.json +56 -0
  180. package/src/advisories.ts +431 -0
  181. package/src/aws-sigv4.ts +174 -0
  182. package/src/baseline.ts +390 -0
  183. package/src/cache.ts +54 -0
  184. package/src/changes.ts +158 -0
  185. package/src/compare.ts +131 -0
  186. package/src/config-schema.ts +451 -0
  187. package/src/config.ts +161 -0
  188. package/src/detect.ts +312 -0
  189. package/src/evaluate.ts +188 -0
  190. package/src/extract.ts +336 -0
  191. package/src/gcp-auth.ts +166 -0
  192. package/src/glob.ts +160 -0
  193. package/src/host.ts +90 -0
  194. package/src/i18n/en.ts +236 -0
  195. package/src/i18n/es.ts +236 -0
  196. package/src/i18n/index.ts +68 -0
  197. package/src/i18n/types.ts +230 -0
  198. package/src/index.ts +228 -0
  199. package/src/llm.ts +708 -0
  200. package/src/nearest.ts +61 -0
  201. package/src/net.ts +233 -0
  202. package/src/node.ts +63 -0
  203. package/src/openrouter.ts +125 -0
  204. package/src/optimize.ts +228 -0
  205. package/src/otlp.ts +179 -0
  206. package/src/phrases.ts +1047 -0
  207. package/src/pricing-overlay.ts +319 -0
  208. package/src/pricing.ts +468 -0
  209. package/src/profile.ts +124 -0
  210. package/src/promptfoo.ts +213 -0
  211. package/src/prune.ts +211 -0
  212. package/src/reorder.ts +307 -0
  213. package/src/review.ts +180 -0
  214. package/src/rules.ts +324 -0
  215. package/src/savings.ts +121 -0
  216. package/src/segment.ts +106 -0
  217. package/src/shared-prefix.ts +198 -0
  218. package/src/similarity.ts +28 -0
  219. package/src/structure.ts +652 -0
  220. package/src/suggest.ts +254 -0
  221. package/src/tokenizer.ts +190 -0
  222. package/src/types.ts +323 -0
  223. package/src/walk.ts +117 -0
@@ -0,0 +1,149 @@
1
+ import { getModel } from './pricing.js';
2
+ import { segment } from './segment.js';
3
+ /** The variable a prompt with no `{{placeholder}}` gets. */
4
+ const APPENDED_VAR = 'input';
5
+ /**
6
+ * promptfoo names providers `<vendor>:<model>`. Only the two vendors whose
7
+ * shape is certain are mapped; anything else is emitted as-is with a warning,
8
+ * because a wrong provider id fails at run time with a message about the
9
+ * harness rather than about this file.
10
+ */
11
+ function providerId(modelId) {
12
+ if (modelId === undefined)
13
+ return { id: 'openai:gpt-4o-mini', warning: null };
14
+ const model = getModel(modelId);
15
+ switch (model.provider) {
16
+ case 'anthropic':
17
+ return { id: `anthropic:messages:${model.id}`, warning: null };
18
+ case 'openai':
19
+ return { id: `openai:${model.id}`, warning: null };
20
+ default:
21
+ return {
22
+ id: model.id,
23
+ warning: `${model.provider ?? 'this provider'} has no known promptfoo id; "${model.id}" is a guess`,
24
+ };
25
+ }
26
+ }
27
+ /** Placeholders in the prompt, in order of appearance, deduplicated. */
28
+ function placeholdersIn(prompt) {
29
+ const found = segment(prompt)
30
+ .filter((piece) => piece.kind === 'protected' && piece.protection === 'placeholder')
31
+ .map((piece) => piece.text);
32
+ return [...new Set(found)];
33
+ }
34
+ /** `{{query}}` → `query`, or null when promptfoo would not substitute it. */
35
+ function varNameOf(placeholder) {
36
+ const match = /^\{\{\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\}\}$/.exec(placeholder);
37
+ return match?.[1] ?? null;
38
+ }
39
+ /**
40
+ * Whether the prompt demands JSON output.
41
+ *
42
+ * Defined narrowly and checkably: a fenced block tagged `json`, or an untagged
43
+ * fenced block whose body parses as JSON. Nothing about the surrounding prose,
44
+ * because "return JSON" in a sentence is a phrase, and a phrase is where
45
+ * guessing starts.
46
+ *
47
+ * The first version of this asked `findRestatedFormat`, which was the wrong
48
+ * question wearing a convenient shape. That function answers "is this prompt
49
+ * wasting tokens restating its own schema?" — so a prompt demanding JSON
50
+ * *cleanly* got no assertion while a wasteful one did, which is exactly
51
+ * backwards.
52
+ */
53
+ function asksForJson(prompt) {
54
+ const fenced = /^[ \t]*(?:```|~~~)([A-Za-z0-9]*)[ \t]*\n([\s\S]*?)^[ \t]*(?:```|~~~)/gm;
55
+ let match;
56
+ while ((match = fenced.exec(prompt)) !== null) {
57
+ const lang = (match[1] ?? '').toLowerCase();
58
+ const body = (match[2] ?? '').trim();
59
+ if (/^jsonc?5?$/.test(lang))
60
+ return true;
61
+ if (lang !== '')
62
+ continue;
63
+ if (!body.startsWith('{') && !body.startsWith('['))
64
+ continue;
65
+ try {
66
+ JSON.parse(body);
67
+ return true;
68
+ }
69
+ catch {
70
+ // An untagged block of something else. Not our business.
71
+ }
72
+ }
73
+ return false;
74
+ }
75
+ export function toPromptfoo(original, optimized, cases, options = {}) {
76
+ const warnings = [];
77
+ const placeholders = placeholdersIn(original);
78
+ const supported = placeholders.map(varNameOf);
79
+ // The variable the cases fill. `trazum eval` substitutes the *first*
80
+ // placeholder and appends when there is none; promptfoo is driven the same
81
+ // way so the two commands are testing the same prompt.
82
+ let variable = APPENDED_VAR;
83
+ let originalTemplate = original;
84
+ let optimizedTemplate = optimized;
85
+ if (placeholders.length === 0) {
86
+ variable = APPENDED_VAR;
87
+ originalTemplate = `${original.trimEnd()}\n\n{{${APPENDED_VAR}}}`;
88
+ optimizedTemplate = `${optimized.trimEnd()}\n\n{{${APPENDED_VAR}}}`;
89
+ warnings.push({
90
+ kind: 'appended-input',
91
+ detail: 'This prompt has no placeholder, so each case is appended at the end — the same ' +
92
+ 'thing `trazum eval` does. If the prompt is meant to be a template, add ' +
93
+ '{{a_variable}} and export again.',
94
+ });
95
+ }
96
+ else {
97
+ const first = supported[0] ?? null;
98
+ if (first === null) {
99
+ warnings.push({
100
+ kind: 'unsupported-placeholder',
101
+ detail: `promptfoo substitutes {{name}}; this prompt uses ${placeholders[0]}, which it will ` +
102
+ 'leave untouched. Every case would run against the literal template.',
103
+ });
104
+ variable = APPENDED_VAR;
105
+ }
106
+ else {
107
+ variable = first;
108
+ }
109
+ const distinct = placeholders.length;
110
+ if (distinct > 1) {
111
+ warnings.push({
112
+ kind: 'multiple-placeholders',
113
+ detail: `The prompt has ${distinct} placeholders (${placeholders.join(', ')}) and the case ` +
114
+ `file supplies one value each, which fills ${placeholders[0]} only. Give the others ` +
115
+ 'defaults in `defaultTest.vars`, or the run tests a prompt nobody sends.',
116
+ });
117
+ }
118
+ }
119
+ const { id, warning } = providerId(options.model);
120
+ if (warning !== null)
121
+ warnings.push({ kind: 'unmapped-provider', detail: warning });
122
+ // The one assertion Trazum can make without guessing at the task: if the
123
+ // prompt asks for JSON, both versions still have to produce it. Everything
124
+ // else — accuracy, refusal rate, format specifics — is the team's to write,
125
+ // and inventing it here would be a tool with opinions about somebody else's
126
+ // product.
127
+ const assertions = asksForJson(original) ? [{ type: 'is-json' }] : [];
128
+ const label = options.level ? `after (trazum, ${options.level})` : 'after (trazum)';
129
+ return {
130
+ config: {
131
+ description: 'Generated by trazum. The only difference between the two prompts is the ' +
132
+ 'optimisation — same provider, same cases, same everything else.\n\n' +
133
+ 'The assertions are yours to write: `trazum eval` already answers "does the model ' +
134
+ 'still say the same thing", and this suite exists for the question it cannot ask — ' +
135
+ 'whether your accuracy, your format and your refusal rate survived. Add them under ' +
136
+ '`defaultTest.assert` or per test.\n\n' +
137
+ 'Docs: https://www.promptfoo.dev/docs/configuration/expected-outputs/',
138
+ prompts: [
139
+ { label: 'before', raw: originalTemplate },
140
+ { label, raw: optimizedTemplate },
141
+ ],
142
+ providers: [id],
143
+ ...(assertions.length > 0 ? { defaultTest: { assert: assertions } } : {}),
144
+ tests: cases.map((input) => ({ vars: { [variable]: input } })),
145
+ },
146
+ warnings,
147
+ };
148
+ }
149
+ //# sourceMappingURL=promptfoo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptfoo.js","sourceRoot":"","sources":["../src/promptfoo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ import type { EvalVerdict } from './evaluate.js';
2
+ import type { LlmProvider, TokenCounter } from './types.js';
3
+ /**
4
+ * Which few-shot examples earn their tokens, measured rather than guessed.
5
+ *
6
+ * The `redundant-examples` advisory answers a *textual* question: does this
7
+ * example look like an earlier one? That catches the way few-shot blocks actually
8
+ * grow — copy the last one, change two fields — and it is the cheapest useful
9
+ * thing to say, because it costs nothing.
10
+ *
11
+ * This answers a different and much stronger question: **does removing this
12
+ * example change any answer?** Two examples can be textually unalike and teach the
13
+ * same thing, and a block nobody has measured is usually where the tokens are: a
14
+ * few-shot section is routinely most of a prompt.
15
+ *
16
+ * The method is leave-one-out against the prompt's own noise floor. Ask the full
17
+ * prompt twice to find out how much the model disagrees with *itself*, then remove
18
+ * one example and ask again. If the answer moves no further than the model already
19
+ * moves on its own, that example is not doing observable work.
20
+ *
21
+ * **It spends the caller's money and must never run by default.** The bill is
22
+ * `2 + examples` calls per input, and `plannedCalls` exists so a caller can print
23
+ * the figure before deciding rather than discovering it afterwards.
24
+ *
25
+ * **What it cannot tell you.** An example may exist for a case these inputs do not
26
+ * contain — the boundary condition somebody hit in production last March and added
27
+ * a demonstration for. Removing it would change nothing measurable here and break
28
+ * that case. So this reports "no effect on these inputs", never "delete this", and
29
+ * the wording is deliberate: the strength of the claim is bounded by the inputs
30
+ * given, and only the caller knows whether those cover what matters.
31
+ */
32
+ export interface ExampleContribution {
33
+ /** Position in the prompt's example block, from zero. */
34
+ index: number;
35
+ /** The example itself, so a report can quote its first line. */
36
+ text: string;
37
+ tokens: number;
38
+ /**
39
+ * Mean agreement between the full prompt's answer and the answer with this
40
+ * example removed, across every input.
41
+ */
42
+ agreementWithout: number;
43
+ /**
44
+ * `indistinguishable` and `within-noise` both mean the removal changed nothing
45
+ * this measurement can see. `diverges` means it did. `inconclusive` means the
46
+ * model disagreed with itself too much for any of this to mean anything.
47
+ */
48
+ verdict: EvalVerdict;
49
+ }
50
+ export interface PruneReport {
51
+ provider: string;
52
+ model: string;
53
+ /** The model's agreement with itself, given the full prompt. The yardstick. */
54
+ selfAgreement: number;
55
+ contributions: ExampleContribution[];
56
+ /** Tokens held by examples whose removal changed nothing observable. */
57
+ recoverableTokens: number;
58
+ /** Calls actually made, so the bill is never a surprise. */
59
+ callsMade: number;
60
+ }
61
+ export interface PruneOptions {
62
+ concurrency?: number;
63
+ countTokens?: TokenCounter;
64
+ }
65
+ /**
66
+ * What this will cost, before it costs it.
67
+ *
68
+ * Pure and exported so a CLI can print the number and let somebody say no. A
69
+ * feature that spends money and only reports the total afterwards is a feature
70
+ * people run once.
71
+ */
72
+ export declare function plannedCalls(examples: number, inputs: number): number;
73
+ /**
74
+ * The prompt with one example block removed, located by position rather than by
75
+ * text.
76
+ *
77
+ * `prompt.replace(block.text, '')` would be shorter and wrong: two identical
78
+ * example blocks — which is exactly what a copy-paste few-shot section contains —
79
+ * would both match the first occurrence, so removing the second would silently
80
+ * remove the first and the measurement would describe a prompt nobody asked about.
81
+ * Scanning forward from the end of the previous block gives each block its true
82
+ * offset.
83
+ */
84
+ export declare function withoutExample(prompt: string, examples: readonly {
85
+ text: string;
86
+ }[], index: number): string;
87
+ /** Thrown rather than returned: there is nothing to report and a reason to say. */
88
+ export declare class NothingToPrune extends Error {
89
+ }
90
+ export declare function pruneExamples(prompt: string, inputs: readonly string[], provider: LlmProvider, options?: PruneOptions): Promise<PruneReport>;
91
+ //# sourceMappingURL=prune.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune.d.ts","sourceRoot":"","sources":["../src/prune.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,mBAAmB;IAClC,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,wEAAwE;IACxE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,YAAY,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAMrE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,EACrC,KAAK,EAAE,MAAM,GACZ,MAAM,CAgBR;AAMD,mFAAmF;AACnF,qBAAa,cAAe,SAAQ,KAAK;CAAG;AAE5C,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,QAAQ,EAAE,WAAW,EACrB,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,WAAW,CAAC,CA8EtB"}
package/dist/prune.js ADDED
@@ -0,0 +1,110 @@
1
+ import { findExamples } from './structure.js';
2
+ import { estimateTokens } from './tokenizer.js';
3
+ import { agreement, pooled, verdictFor } from './evaluate.js';
4
+ /**
5
+ * What this will cost, before it costs it.
6
+ *
7
+ * Pure and exported so a CLI can print the number and let somebody say no. A
8
+ * feature that spends money and only reports the total afterwards is a feature
9
+ * people run once.
10
+ */
11
+ export function plannedCalls(examples, inputs) {
12
+ if (examples < 2 || inputs < 1)
13
+ return 0;
14
+ // Two baseline runs per input to establish the noise floor, then one run per
15
+ // example removed. The baselines are shared across every example, which is the
16
+ // only reason this is affordable at all.
17
+ return inputs * (2 + examples);
18
+ }
19
+ /**
20
+ * The prompt with one example block removed, located by position rather than by
21
+ * text.
22
+ *
23
+ * `prompt.replace(block.text, '')` would be shorter and wrong: two identical
24
+ * example blocks — which is exactly what a copy-paste few-shot section contains —
25
+ * would both match the first occurrence, so removing the second would silently
26
+ * remove the first and the measurement would describe a prompt nobody asked about.
27
+ * Scanning forward from the end of the previous block gives each block its true
28
+ * offset.
29
+ */
30
+ export function withoutExample(prompt, examples, index) {
31
+ let cursor = 0;
32
+ for (let position = 0; position < examples.length; position++) {
33
+ const text = examples[position]?.text ?? '';
34
+ const at = prompt.indexOf(text, cursor);
35
+ if (at === -1)
36
+ break;
37
+ if (position === index) {
38
+ const before = prompt.slice(0, at);
39
+ const after = prompt.slice(at + text.length);
40
+ // Collapse the blank lines the removal leaves behind, so the resulting
41
+ // prompt is one an author might have written rather than one with a hole.
42
+ return `${before}${after}`.replace(/\n{3,}/g, '\n\n');
43
+ }
44
+ cursor = at + text.length;
45
+ }
46
+ return prompt;
47
+ }
48
+ function mean(values) {
49
+ return values.length === 0 ? 0 : values.reduce((sum, value) => sum + value, 0) / values.length;
50
+ }
51
+ /** Thrown rather than returned: there is nothing to report and a reason to say. */
52
+ export class NothingToPrune extends Error {
53
+ }
54
+ export async function pruneExamples(prompt, inputs, provider, options = {}) {
55
+ const count = options.countTokens ?? estimateTokens;
56
+ const concurrency = Math.max(1, options.concurrency ?? 3);
57
+ const examples = findExamples(prompt, count);
58
+ if (examples.length < 2) {
59
+ throw new NothingToPrune('this prompt has fewer than two few-shot examples, so there is nothing to compare');
60
+ }
61
+ if (inputs.length === 0) {
62
+ throw new NothingToPrune('no inputs were given, and leave-one-out needs something to run on');
63
+ }
64
+ let callsMade = 0;
65
+ const run = async (text, input) => {
66
+ callsMade++;
67
+ return provider.complete({ system: text.replaceAll('{{input}}', input), user: input });
68
+ };
69
+ /**
70
+ * Baselines first, and sequentially within an input.
71
+ *
72
+ * The two runs exist to measure the model's own variance, and issuing them
73
+ * together invites a provider to serve one from a cache and report a variance of
74
+ * zero — which would make every example look load-bearing.
75
+ */
76
+ const baselines = await pooled(inputs.map((input) => async () => {
77
+ const first = await run(prompt, input);
78
+ const second = await run(prompt, input);
79
+ return { input, first, selfSimilarity: agreement(first, second) };
80
+ }), concurrency);
81
+ const selfAgreement = mean(baselines.map((baseline) => baseline.selfSimilarity));
82
+ const contributions = await pooled(examples.map((example, index) => async () => {
83
+ const reduced = withoutExample(prompt, examples, index);
84
+ const answers = await pooled(baselines.map((baseline) => () => run(reduced, baseline.input)), 1);
85
+ const agreementWithout = mean(answers.map((answer, position) => agreement(baselines[position].first, answer)));
86
+ return {
87
+ index,
88
+ text: example.text,
89
+ tokens: example.tokens,
90
+ agreementWithout,
91
+ // `verdictFor` and `agreement` both come from evaluate.ts rather than
92
+ // being reimplemented. A repository where two features disagree about what
93
+ // "within the noise" means is one where the answer depends on which
94
+ // command you ran — and the first draft of this file did exactly that.
95
+ verdict: verdictFor(selfAgreement, agreementWithout),
96
+ };
97
+ }), concurrency);
98
+ const recoverableTokens = contributions
99
+ .filter((contribution) => contribution.verdict === 'indistinguishable' || contribution.verdict === 'within-noise')
100
+ .reduce((sum, contribution) => sum + contribution.tokens, 0);
101
+ return {
102
+ provider: provider.name,
103
+ model: provider.model,
104
+ selfAgreement,
105
+ contributions,
106
+ recoverableTokens,
107
+ callsMade,
108
+ };
109
+ }
110
+ //# sourceMappingURL=prune.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune.js","sourceRoot":"","sources":["../src/prune.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ import type { TokenCounter } from './types.js';
2
+ /**
3
+ * Moving stable instructions in front of the first placeholder.
4
+ *
5
+ * This is the largest saving Trazum knows about and the only one it used to
6
+ * report without acting on. Prompt caching is a byte-for-byte prefix match, so
7
+ * everything after the first `{{placeholder}}` is re-read at full price on every
8
+ * call. Measured on a 1,178-token support prompt: 14 tokens cacheable as written,
9
+ * 1,174 after rearranging the *same content* — $227.65 a month at 50,000 calls.
10
+ *
11
+ * No rule can compete with that, because a rule deletes a few percent of tokens
12
+ * while this changes the price of 98% of them.
13
+ *
14
+ * **It is also the most dangerous thing in this repository**, which is why it is
15
+ * not a rule and not part of `aggressive`. Every other transformation removes
16
+ * text whose absence is local. This one moves text, and order carries meaning:
17
+ * "Summarise the text above" is correct where it sits and nonsense in front of
18
+ * the text it points at. So the whole design here is about what to *refuse*.
19
+ *
20
+ * Three refusals, in order of how much they cost:
21
+ *
22
+ * 1. **A block containing a backward reference stays put** — and so does
23
+ * everything after it. Moving a later block past a pinned one changes their
24
+ * order relative to each other, which is the same class of harm.
25
+ * 2. **Only whole blocks move.** Blocks are separated by blank lines, so a
26
+ * sentence is never severed from the paragraph that qualifies it.
27
+ * 3. **Nothing moves if the prompt has no placeholder**, or if the resulting
28
+ * prefix would not clear the model's cacheable minimum anyway — a
29
+ * rearrangement that buys nothing is a diff for its own sake.
30
+ */
31
+ export interface ReorderedBlock {
32
+ text: string;
33
+ tokens: number;
34
+ }
35
+ export interface DeclinedBlock {
36
+ text: string;
37
+ /**
38
+ * Why it stayed. `backward-reference` names the phrase found; `after-pinned`
39
+ * means an earlier block was pinned and moving this one would reorder the two.
40
+ */
41
+ reason: 'backward-reference' | 'after-pinned' | 'uncovered-script';
42
+ /** The phrase that pinned it, for `backward-reference`. */
43
+ phrase?: string;
44
+ /** The script with no phrase list, for `uncovered-script`. */
45
+ script?: string;
46
+ }
47
+ export interface ReorderResult {
48
+ /** The rearranged prompt, or the original when nothing could move. */
49
+ text: string;
50
+ /** Blocks moved ahead of the first placeholder, in their original order. */
51
+ moved: ReorderedBlock[];
52
+ /** Blocks left where they were, with the reason. */
53
+ declined: DeclinedBlock[];
54
+ /** Tokens that moved from unpriced-every-call into the cacheable prefix. */
55
+ tokensMoved: number;
56
+ /** Cacheable prefix before and after, so the gain is visible rather than claimed. */
57
+ prefixTokensBefore: number;
58
+ prefixTokensAfter: number;
59
+ }
60
+ export interface ReorderOptions {
61
+ count?: TokenCounter;
62
+ /**
63
+ * Do not rearrange unless the prefix ends up at least this long. Defaults to
64
+ * 0 — the caller knows the model's cacheable minimum and this module does not.
65
+ *
66
+ * The bar is on the **resulting prefix**, not on the amount moved. Those are
67
+ * different questions, and asking the second one refuses a real saving: a
68
+ * prompt whose head already clears the minimum gains from any block that joins
69
+ * it, however small. Asking "did 200 tokens move?" answers "no" and reports
70
+ * that nothing could move, which is not what happened.
71
+ */
72
+ minPrefixTokens?: number;
73
+ }
74
+ /**
75
+ * Rearranges a prompt so its stable instructions sit in the cacheable prefix.
76
+ *
77
+ * Returns the original text unchanged when nothing can safely move, and always
78
+ * reports what it declined and why — a saving Trazum silently chose not to take
79
+ * is one the author cannot evaluate.
80
+ */
81
+ export declare function reorderForCache(prompt: string, options?: ReorderOptions): ReorderResult;
82
+ //# sourceMappingURL=reorder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reorder.d.ts","sourceRoot":"","sources":["../src/reorder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,MAAM,EAAE,oBAAoB,GAAG,cAAc,GAAG,kBAAkB,CAAC;IACnE,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,oDAAoD;IACpD,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,qFAAqF;IACrF,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAiGD,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,aAAa,CA0H3F"}