@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.
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/advisories.d.ts +23 -0
- package/dist/advisories.d.ts.map +1 -0
- package/dist/advisories.js +376 -0
- package/dist/advisories.js.map +1 -0
- package/dist/aws-sigv4.d.ts +88 -0
- package/dist/aws-sigv4.d.ts.map +1 -0
- package/dist/aws-sigv4.js +117 -0
- package/dist/aws-sigv4.js.map +1 -0
- package/dist/baseline.d.ts +171 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +273 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cache.d.ts +26 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +28 -0
- package/dist/cache.js.map +1 -0
- package/dist/changes.d.ts +29 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +142 -0
- package/dist/changes.js.map +1 -0
- package/dist/compare.d.ts +65 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +58 -0
- package/dist/compare.js.map +1 -0
- package/dist/config-schema.d.ts +118 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config-schema.js +315 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +112 -0
- package/dist/config.js.map +1 -0
- package/dist/detect.d.ts +70 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +228 -0
- package/dist/detect.js.map +1 -0
- package/dist/evaluate.d.ts +98 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +110 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/extract.d.ts +81 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +280 -0
- package/dist/extract.js.map +1 -0
- package/dist/gcp-auth.d.ts +58 -0
- package/dist/gcp-auth.d.ts.map +1 -0
- package/dist/gcp-auth.js +113 -0
- package/dist/gcp-auth.js.map +1 -0
- package/dist/glob.d.ts +49 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +154 -0
- package/dist/glob.js.map +1 -0
- package/dist/host.d.ts +30 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +69 -0
- package/dist/host.js.map +1 -0
- package/dist/i18n/en.d.ts +4 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +168 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/es.d.ts +4 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +168 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/index.d.ts +36 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +50 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/types.d.ts +180 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +11 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/llm.d.ts +226 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +485 -0
- package/dist/llm.js.map +1 -0
- package/dist/nearest.d.ts +20 -0
- package/dist/nearest.d.ts.map +1 -0
- package/dist/nearest.js +54 -0
- package/dist/nearest.js.map +1 -0
- package/dist/net.d.ts +90 -0
- package/dist/net.d.ts.map +1 -0
- package/dist/net.js +203 -0
- package/dist/net.js.map +1 -0
- package/dist/node.d.ts +32 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +33 -0
- package/dist/node.js.map +1 -0
- package/dist/openrouter.d.ts +25 -0
- package/dist/openrouter.d.ts.map +1 -0
- package/dist/openrouter.js +72 -0
- package/dist/openrouter.js.map +1 -0
- package/dist/optimize.d.ts +38 -0
- package/dist/optimize.d.ts.map +1 -0
- package/dist/optimize.js +183 -0
- package/dist/optimize.js.map +1 -0
- package/dist/otlp.d.ts +91 -0
- package/dist/otlp.d.ts.map +1 -0
- package/dist/otlp.js +102 -0
- package/dist/otlp.js.map +1 -0
- package/dist/phrases.d.ts +169 -0
- package/dist/phrases.d.ts.map +1 -0
- package/dist/phrases.js +939 -0
- package/dist/phrases.js.map +1 -0
- package/dist/pricing-overlay.d.ts +55 -0
- package/dist/pricing-overlay.d.ts.map +1 -0
- package/dist/pricing-overlay.js +241 -0
- package/dist/pricing-overlay.js.map +1 -0
- package/dist/pricing.d.ts +115 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +400 -0
- package/dist/pricing.js.map +1 -0
- package/dist/profile.d.ts +71 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +55 -0
- package/dist/profile.js.map +1 -0
- package/dist/promptfoo.d.ts +58 -0
- package/dist/promptfoo.d.ts.map +1 -0
- package/dist/promptfoo.js +149 -0
- package/dist/promptfoo.js.map +1 -0
- package/dist/prune.d.ts +91 -0
- package/dist/prune.d.ts.map +1 -0
- package/dist/prune.js +110 -0
- package/dist/prune.js.map +1 -0
- package/dist/reorder.d.ts +82 -0
- package/dist/reorder.d.ts.map +1 -0
- package/dist/reorder.js +215 -0
- package/dist/reorder.js.map +1 -0
- package/dist/review.d.ts +54 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +131 -0
- package/dist/review.js.map +1 -0
- package/dist/rules.d.ts +5 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +279 -0
- package/dist/rules.js.map +1 -0
- package/dist/savings.d.ts +36 -0
- package/dist/savings.d.ts.map +1 -0
- package/dist/savings.js +83 -0
- package/dist/savings.js.map +1 -0
- package/dist/segment.d.ts +8 -0
- package/dist/segment.d.ts.map +1 -0
- package/dist/segment.js +74 -0
- package/dist/segment.js.map +1 -0
- package/dist/shared-prefix.d.ts +63 -0
- package/dist/shared-prefix.d.ts.map +1 -0
- package/dist/shared-prefix.js +151 -0
- package/dist/shared-prefix.js.map +1 -0
- package/dist/similarity.d.ts +13 -0
- package/dist/similarity.d.ts.map +1 -0
- package/dist/similarity.js +30 -0
- package/dist/similarity.js.map +1 -0
- package/dist/structure.d.ts +144 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +455 -0
- package/dist/structure.js.map +1 -0
- package/dist/suggest.d.ts +100 -0
- package/dist/suggest.d.ts.map +1 -0
- package/dist/suggest.js +151 -0
- package/dist/suggest.js.map +1 -0
- package/dist/tokenizer.d.ts +57 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +157 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/walk.d.ts +40 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +95 -0
- package/dist/walk.js.map +1 -0
- package/package.json +56 -0
- package/src/advisories.ts +431 -0
- package/src/aws-sigv4.ts +174 -0
- package/src/baseline.ts +390 -0
- package/src/cache.ts +54 -0
- package/src/changes.ts +158 -0
- package/src/compare.ts +131 -0
- package/src/config-schema.ts +451 -0
- package/src/config.ts +161 -0
- package/src/detect.ts +312 -0
- package/src/evaluate.ts +188 -0
- package/src/extract.ts +336 -0
- package/src/gcp-auth.ts +166 -0
- package/src/glob.ts +160 -0
- package/src/host.ts +90 -0
- package/src/i18n/en.ts +236 -0
- package/src/i18n/es.ts +236 -0
- package/src/i18n/index.ts +68 -0
- package/src/i18n/types.ts +230 -0
- package/src/index.ts +228 -0
- package/src/llm.ts +708 -0
- package/src/nearest.ts +61 -0
- package/src/net.ts +233 -0
- package/src/node.ts +63 -0
- package/src/openrouter.ts +125 -0
- package/src/optimize.ts +228 -0
- package/src/otlp.ts +179 -0
- package/src/phrases.ts +1047 -0
- package/src/pricing-overlay.ts +319 -0
- package/src/pricing.ts +468 -0
- package/src/profile.ts +124 -0
- package/src/promptfoo.ts +213 -0
- package/src/prune.ts +211 -0
- package/src/reorder.ts +307 -0
- package/src/review.ts +180 -0
- package/src/rules.ts +324 -0
- package/src/savings.ts +121 -0
- package/src/segment.ts +106 -0
- package/src/shared-prefix.ts +198 -0
- package/src/similarity.ts +28 -0
- package/src/structure.ts +652 -0
- package/src/suggest.ts +254 -0
- package/src/tokenizer.ts +190 -0
- package/src/types.ts +323 -0
- 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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"}
|
package/dist/prune.d.ts
ADDED
|
@@ -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"}
|