@trazum/core 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -5
- package/dist/advisories.d.ts.map +1 -1
- package/dist/advisories.js +94 -4
- package/dist/advisories.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +18 -7
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +18 -7
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +28 -1
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tokenizer.d.ts +33 -26
- package/dist/tokenizer.d.ts.map +1 -1
- package/dist/tokenizer.js +95 -34
- package/dist/tokenizer.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +219 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +274 -0
- package/dist/usage.js.map +1 -0
- package/package.json +1 -1
- package/src/advisories.ts +96 -4
- package/src/i18n/en.ts +19 -6
- package/src/i18n/es.ts +19 -6
- package/src/i18n/types.ts +29 -1
- package/src/index.ts +14 -0
- package/src/tokenizer.ts +93 -30
- package/src/types.ts +1 -0
- package/src/usage.ts +479 -0
package/dist/usage.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
2
|
+
const EMPTY = () => ({
|
|
3
|
+
calls: 0,
|
|
4
|
+
inputTokens: 0,
|
|
5
|
+
cacheReadTokens: 0,
|
|
6
|
+
cacheWriteTokens: 0,
|
|
7
|
+
outputTokens: 0,
|
|
8
|
+
assumedWriteTtlCalls: 0,
|
|
9
|
+
inputUsd: 0,
|
|
10
|
+
cacheReadUsd: 0,
|
|
11
|
+
cacheWriteUsd: 0,
|
|
12
|
+
outputUsd: 0,
|
|
13
|
+
totalUsd: 0,
|
|
14
|
+
});
|
|
15
|
+
const OK = (value) => ({ kind: 'ok', value });
|
|
16
|
+
function readCount(...candidates) {
|
|
17
|
+
let sawCorrupt = false;
|
|
18
|
+
for (const value of candidates) {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
continue;
|
|
21
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
|
|
22
|
+
return OK(value);
|
|
23
|
+
// Present and unusable: a string, a null, a negative, a NaN.
|
|
24
|
+
sawCorrupt = true;
|
|
25
|
+
}
|
|
26
|
+
return sawCorrupt ? { kind: 'corrupt' } : { kind: 'absent' };
|
|
27
|
+
}
|
|
28
|
+
/** Zero for an absent count. Callers reject corrupt ones before reaching this. */
|
|
29
|
+
const valueOf = (count) => (count.kind === 'ok' ? count.value : 0);
|
|
30
|
+
/**
|
|
31
|
+
* One line of a usage log, or `null` when it is not one.
|
|
32
|
+
*
|
|
33
|
+
* Accepts the Anthropic shape and the OpenAI one, because those are the two
|
|
34
|
+
* things people actually have. The alternative — a Trazum-specific schema — asks
|
|
35
|
+
* for a transformation step before the tool will read anything, and a tool with a
|
|
36
|
+
* setup cost that exceeds its payoff does not get run twice.
|
|
37
|
+
*
|
|
38
|
+
* `null` in three cases, and the third is the one that was wrong:
|
|
39
|
+
*
|
|
40
|
+
* 1. Not JSON, or not an object, or no `model`.
|
|
41
|
+
* 2. **No** token counts at all — counting it would inflate the call count while
|
|
42
|
+
* contributing nothing, which lowers every per-call figure.
|
|
43
|
+
* 3. **Any** count present but unreadable. A field that is there and unusable is
|
|
44
|
+
* corruption, and a corrupt line belongs in `skippedLines` where the report
|
|
45
|
+
* names it, not in the totals as a silent zero.
|
|
46
|
+
*/
|
|
47
|
+
export function parseUsageLine(line) {
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = JSON.parse(line);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
56
|
+
return null;
|
|
57
|
+
const record = raw;
|
|
58
|
+
// Anthropic nests usage on a response; a hand-rolled log usually flattens it.
|
|
59
|
+
const usage = typeof record.usage === 'object' && record.usage !== null
|
|
60
|
+
? record.usage
|
|
61
|
+
: record;
|
|
62
|
+
const model = typeof record.model === 'string' ? record.model : null;
|
|
63
|
+
if (!model)
|
|
64
|
+
return null;
|
|
65
|
+
/**
|
|
66
|
+
* OpenAI reports cached tokens inside `prompt_tokens_details` **and counts them
|
|
67
|
+
* in `prompt_tokens`**, while Anthropic reports them separately and does not.
|
|
68
|
+
* Subtracting in one case and not the other is the difference between a correct
|
|
69
|
+
* bill and one that charges the cached half twice.
|
|
70
|
+
*/
|
|
71
|
+
const details = typeof usage.prompt_tokens_details === 'object' && usage.prompt_tokens_details !== null
|
|
72
|
+
? usage.prompt_tokens_details
|
|
73
|
+
: null;
|
|
74
|
+
const openAiCached = details ? readCount(details.cached_tokens) : { kind: 'absent' };
|
|
75
|
+
/**
|
|
76
|
+
* Anthropic splits cache writes by time-to-live, and the two cost different
|
|
77
|
+
* amounts: 1.25x input for the 5-minute entry, **2x** for the 1-hour one.
|
|
78
|
+
*
|
|
79
|
+
* Reading only the flat `cache_creation_input_tokens` threw that distinction
|
|
80
|
+
* away and then priced everything at the cheaper rate — a 1-hour workload
|
|
81
|
+
* reported 37.5% under, silently, on its largest line. The split is in the log
|
|
82
|
+
* whenever the recording recipe in the README is followed, because it is part of
|
|
83
|
+
* the `usage` object the API returns.
|
|
84
|
+
*/
|
|
85
|
+
const creation = typeof usage.cache_creation === 'object' && usage.cache_creation !== null
|
|
86
|
+
? usage.cache_creation
|
|
87
|
+
: null;
|
|
88
|
+
const write5m = creation ? readCount(creation.ephemeral_5m_input_tokens) : { kind: 'absent' };
|
|
89
|
+
const write1h = creation ? readCount(creation.ephemeral_1h_input_tokens) : { kind: 'absent' };
|
|
90
|
+
const counts = {
|
|
91
|
+
input: readCount(usage.input_tokens, usage.inputTokens, usage.prompt_tokens),
|
|
92
|
+
output: readCount(usage.output_tokens, usage.outputTokens, usage.completion_tokens),
|
|
93
|
+
cacheRead: readCount(usage.cache_read_input_tokens, usage.cacheReadTokens),
|
|
94
|
+
cacheWrite: readCount(usage.cache_creation_input_tokens, usage.cacheWriteTokens),
|
|
95
|
+
openAiCached,
|
|
96
|
+
write5m,
|
|
97
|
+
write1h,
|
|
98
|
+
};
|
|
99
|
+
// Any field present and unreadable rejects the line. See `readCount`.
|
|
100
|
+
if (Object.values(counts).some((c) => c.kind === 'corrupt'))
|
|
101
|
+
return null;
|
|
102
|
+
// Nothing to count at all.
|
|
103
|
+
if (Object.values(counts).every((c) => c.kind === 'absent'))
|
|
104
|
+
return null;
|
|
105
|
+
const cached = valueOf(counts.openAiCached);
|
|
106
|
+
const flatWrite = valueOf(counts.cacheWrite);
|
|
107
|
+
const split5m = valueOf(counts.write5m);
|
|
108
|
+
const split1h = valueOf(counts.write1h);
|
|
109
|
+
const hasSplit = counts.write5m.kind === 'ok' || counts.write1h.kind === 'ok';
|
|
110
|
+
return {
|
|
111
|
+
model,
|
|
112
|
+
inputTokens: Math.max(0, valueOf(counts.input) - cached),
|
|
113
|
+
cacheReadTokens: counts.cacheRead.kind === 'ok' ? counts.cacheRead.value : cached,
|
|
114
|
+
/**
|
|
115
|
+
* The split when the log carries it, the flat number otherwise — and
|
|
116
|
+
* `writeTtlKnown` says which, so the report can admit that a rate was assumed
|
|
117
|
+
* rather than quietly choosing the cheaper one.
|
|
118
|
+
*/
|
|
119
|
+
cacheWrite5mTokens: hasSplit ? split5m : flatWrite,
|
|
120
|
+
cacheWrite1hTokens: hasSplit ? split1h : 0,
|
|
121
|
+
writeTtlKnown: hasSplit || flatWrite === 0,
|
|
122
|
+
outputTokens: valueOf(counts.output),
|
|
123
|
+
label: typeof record.label === 'string' && record.label.trim() !== ''
|
|
124
|
+
? record.label.trim()
|
|
125
|
+
: null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** The bucket unlabelled calls land in, named so a report can say so. */
|
|
129
|
+
export const UNLABELLED = 'unlabelled';
|
|
130
|
+
/** Token counts only. Used for both halves, because both need them. */
|
|
131
|
+
function countInto(into, record) {
|
|
132
|
+
into.calls += 1;
|
|
133
|
+
into.inputTokens += record.inputTokens;
|
|
134
|
+
into.cacheReadTokens += record.cacheReadTokens;
|
|
135
|
+
into.cacheWriteTokens += record.cacheWrite5mTokens + record.cacheWrite1hTokens;
|
|
136
|
+
if (!record.writeTtlKnown)
|
|
137
|
+
into.assumedWriteTtlCalls += 1;
|
|
138
|
+
into.outputTokens += record.outputTokens;
|
|
139
|
+
}
|
|
140
|
+
function add(into, record, catalogue, on) {
|
|
141
|
+
/**
|
|
142
|
+
* Looked up directly rather than through `modelFrom`, which **throws** on an id
|
|
143
|
+
* it does not know. A usage log is somebody's production traffic and will
|
|
144
|
+
* contain models this catalogue has never heard of — a fine-tune, a preview, a
|
|
145
|
+
* competitor. Throwing means one unfamiliar id destroys the whole profile;
|
|
146
|
+
* naming it separately means the report is honest about what it could not price
|
|
147
|
+
* and useful about everything else.
|
|
148
|
+
*
|
|
149
|
+
* **Priced first, counted second.** The other order was the bug: counts landed
|
|
150
|
+
* before the lookup could fail, so an unpriced call contributed tokens to a
|
|
151
|
+
* total whose dollars excluded it.
|
|
152
|
+
*/
|
|
153
|
+
const model = catalogue.byId.get(record.model);
|
|
154
|
+
if (!model)
|
|
155
|
+
return false;
|
|
156
|
+
countInto(into, record);
|
|
157
|
+
const { inputPerMTok, outputPerMTok } = effectivePricing(model, on);
|
|
158
|
+
const rates = multipliersFor(model);
|
|
159
|
+
const per = (tokens, rate) => (tokens / 1_000_000) * rate;
|
|
160
|
+
into.inputUsd += per(record.inputTokens, inputPerMTok);
|
|
161
|
+
into.cacheReadUsd += per(record.cacheReadTokens, inputPerMTok * rates.cacheRead);
|
|
162
|
+
/**
|
|
163
|
+
* Each TTL at its own rate. Anthropic charges 1.25x input for a 5-minute entry
|
|
164
|
+
* and 2x for a 1-hour one, and the first version applied 1.25x to both — 37.5%
|
|
165
|
+
* under on a 1-hour workload, on the largest line, with nothing on screen
|
|
166
|
+
* saying a rate had been chosen.
|
|
167
|
+
*/
|
|
168
|
+
into.cacheWriteUsd += per(record.cacheWrite5mTokens, inputPerMTok * rates.cacheWrite5m);
|
|
169
|
+
into.cacheWriteUsd += per(record.cacheWrite1hTokens, inputPerMTok * rates.cacheWrite1h);
|
|
170
|
+
into.outputUsd += per(record.outputTokens, outputPerMTok);
|
|
171
|
+
into.totalUsd =
|
|
172
|
+
into.inputUsd + into.cacheReadUsd + into.cacheWriteUsd + into.outputUsd;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Reads a usage log and says where the money went.
|
|
177
|
+
*
|
|
178
|
+
* Takes the whole text rather than a stream: a usage log is measured in megabytes
|
|
179
|
+
* and this package imports no Node builtins, so streaming would mean an interface
|
|
180
|
+
* the browser build cannot satisfy. `@trazum/core/node` is where file reading
|
|
181
|
+
* lives, and it can chunk if it ever needs to.
|
|
182
|
+
*/
|
|
183
|
+
export function profileUsage(text, options) {
|
|
184
|
+
const { catalogue, on = new Date() } = options;
|
|
185
|
+
const total = EMPTY();
|
|
186
|
+
const unpriced = EMPTY();
|
|
187
|
+
const byLabel = new Map();
|
|
188
|
+
const byModel = new Map();
|
|
189
|
+
const unpricedModels = new Set();
|
|
190
|
+
const skippedLines = [];
|
|
191
|
+
const lines = text.split('\n');
|
|
192
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
193
|
+
const line = lines[i].trim();
|
|
194
|
+
if (line === '')
|
|
195
|
+
continue;
|
|
196
|
+
const record = parseUsageLine(line);
|
|
197
|
+
if (!record) {
|
|
198
|
+
skippedLines.push(i + 1);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (!add(total, record, catalogue, on)) {
|
|
202
|
+
unpricedModels.add(record.model);
|
|
203
|
+
countInto(unpriced, record);
|
|
204
|
+
// Still grouped by model, so the reader can see which unknown id is costing
|
|
205
|
+
// them attention — but with zero dollars, which the grouping makes obvious.
|
|
206
|
+
if (!byModel.has(record.model))
|
|
207
|
+
byModel.set(record.model, EMPTY());
|
|
208
|
+
countInto(byModel.get(record.model), record);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const labelKey = record.label ?? UNLABELLED;
|
|
212
|
+
if (!byLabel.has(labelKey))
|
|
213
|
+
byLabel.set(labelKey, EMPTY());
|
|
214
|
+
add(byLabel.get(labelKey), record, catalogue, on);
|
|
215
|
+
if (!byModel.has(record.model))
|
|
216
|
+
byModel.set(record.model, EMPTY());
|
|
217
|
+
add(byModel.get(record.model), record, catalogue, on);
|
|
218
|
+
}
|
|
219
|
+
const sorted = (map, key) => [...map.entries()]
|
|
220
|
+
.sort((a, b) => b[1].totalUsd - a[1].totalUsd || a[0].localeCompare(b[0]))
|
|
221
|
+
.map(([name, breakdown]) => ({ [key]: name, breakdown }));
|
|
222
|
+
return {
|
|
223
|
+
total,
|
|
224
|
+
byLabel: sorted(byLabel, 'label'),
|
|
225
|
+
byModel: sorted(byModel, 'model'),
|
|
226
|
+
unpricedModels: [...unpricedModels].sort(),
|
|
227
|
+
unpriced,
|
|
228
|
+
skippedLines,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* What share of the bill each part is.
|
|
233
|
+
*
|
|
234
|
+
* The point of the whole module in one function: a caller can print "output is
|
|
235
|
+
* 87% of this" without doing arithmetic that would drift from the arithmetic
|
|
236
|
+
* here.
|
|
237
|
+
*
|
|
238
|
+
* All zeroes when nothing was spent, rather than `NaN`. A profile of an empty log
|
|
239
|
+
* is a legitimate result — no calls yet — and a report full of `NaN%` is a bug
|
|
240
|
+
* report from somebody who did nothing wrong.
|
|
241
|
+
*/
|
|
242
|
+
export function sharesOf(breakdown) {
|
|
243
|
+
const { totalUsd } = breakdown;
|
|
244
|
+
if (totalUsd <= 0)
|
|
245
|
+
return { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
|
|
246
|
+
return {
|
|
247
|
+
input: breakdown.inputUsd / totalUsd,
|
|
248
|
+
cacheRead: breakdown.cacheReadUsd / totalUsd,
|
|
249
|
+
cacheWrite: breakdown.cacheWriteUsd / totalUsd,
|
|
250
|
+
output: breakdown.outputUsd / totalUsd,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* How much of the input that could have been cached was.
|
|
255
|
+
*
|
|
256
|
+
* `null` when nothing was cacheable-looking at all — no reads and no writes —
|
|
257
|
+
* because a hit rate over zero attempts is not zero, it is undefined, and
|
|
258
|
+
* printing "0% cache hit rate" for somebody who never turned caching on is a
|
|
259
|
+
* finding about nothing.
|
|
260
|
+
*
|
|
261
|
+
* Reads against reads-plus-full-price-input, deliberately. Cache *writes* are
|
|
262
|
+
* excluded from the denominator: a write is the cost of establishing an entry,
|
|
263
|
+
* not a missed read, and counting it as a miss makes a healthy cache look broken
|
|
264
|
+
* on the day it warms.
|
|
265
|
+
*/
|
|
266
|
+
export function cacheHitRate(breakdown) {
|
|
267
|
+
const attempts = breakdown.cacheReadTokens + breakdown.inputTokens;
|
|
268
|
+
if (breakdown.cacheReadTokens === 0 && breakdown.cacheWriteTokens === 0)
|
|
269
|
+
return null;
|
|
270
|
+
if (attempts === 0)
|
|
271
|
+
return null;
|
|
272
|
+
return breakdown.cacheReadTokens / attempts;
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=usage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Trazum core: priced advisories for LLM prompts (caching, model tier, batching, schemas), plus deterministic trimming, token counting and pricing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "David Mu\u00f1oz Rey",
|
package/src/advisories.ts
CHANGED
|
@@ -125,7 +125,25 @@ export function buildAdvisories(
|
|
|
125
125
|
const monthlyOutputUsd =
|
|
126
126
|
(usage.avgOutputTokens / 1_000_000) * outputPerMTok * usage.callsPerMonth * batchFactor;
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
/**
|
|
129
|
+
* --- Context window ---
|
|
130
|
+
*
|
|
131
|
+
* The third place an estimate was compared against a hard threshold and the
|
|
132
|
+
* answer stated as fact, after `cache-prefix-reorder` and `prompt-caching`. This
|
|
133
|
+
* one has no dollar figure and is the most absolute of the three: **"The call
|
|
134
|
+
* will fail."**
|
|
135
|
+
*
|
|
136
|
+
* With a ±10% band it fails in both directions. An estimated 1,050,000 tokens
|
|
137
|
+
* against a 1,000,000 window can truly be 945,000 — the call succeeds and the
|
|
138
|
+
* reader has been sent to split a prompt that fitted. And an estimated 990,000
|
|
139
|
+
* can truly be 1,089,000, which does not fit, and nothing said anything at all.
|
|
140
|
+
*
|
|
141
|
+
* The silent direction is the worse one. A prompt over the window fails
|
|
142
|
+
* outright rather than degrading, so there is no partial result to notice.
|
|
143
|
+
*/
|
|
144
|
+
const estimated = count === estimateTokens;
|
|
145
|
+
const band = ESTIMATE_ERROR_BAND_PCT / 100;
|
|
146
|
+
|
|
129
147
|
if (tokensAfter > model.contextWindow) {
|
|
130
148
|
advisories.push({
|
|
131
149
|
id: 'context-overflow',
|
|
@@ -134,6 +152,20 @@ export function buildAdvisories(
|
|
|
134
152
|
tokens: tokensAfter,
|
|
135
153
|
modelName: model.displayName,
|
|
136
154
|
contextWindow: model.contextWindow,
|
|
155
|
+
// Only an estimate can be uncertain. A caller who counted exactly is told
|
|
156
|
+
// the call fails, because it does.
|
|
157
|
+
uncertain: estimated && tokensAfter * (1 - band) <= model.contextWindow,
|
|
158
|
+
}),
|
|
159
|
+
estimatedMonthlyUsd: null,
|
|
160
|
+
});
|
|
161
|
+
} else if (estimated && tokensAfter * (1 + band) > model.contextWindow) {
|
|
162
|
+
advisories.push({
|
|
163
|
+
id: 'context-near-limit',
|
|
164
|
+
severity: 'warning',
|
|
165
|
+
...t.advisories.contextNearLimit({
|
|
166
|
+
tokens: tokensAfter,
|
|
167
|
+
modelName: model.displayName,
|
|
168
|
+
contextWindow: model.contextWindow,
|
|
137
169
|
}),
|
|
138
170
|
estimatedMonthlyUsd: null,
|
|
139
171
|
});
|
|
@@ -185,6 +217,22 @@ export function buildAdvisories(
|
|
|
185
217
|
readPct: Math.round(rates.cacheRead * 100),
|
|
186
218
|
writePct: Math.round(rates.cacheWrite5m * 100),
|
|
187
219
|
explicit: (model.caching ?? 'explicit') === 'explicit',
|
|
220
|
+
/**
|
|
221
|
+
* The mirror of `couldReachMinimum` on `below-cache-minimum`, and the
|
|
222
|
+
* asymmetry between them was a real gap: that one hedged an estimate
|
|
223
|
+
* landing just *under* the threshold, while this one promised money on
|
|
224
|
+
* an estimate landing just *over* it. With a ±10% band an estimated
|
|
225
|
+
* 528-token prefix can truly be 475, and then nothing caches at all.
|
|
226
|
+
*
|
|
227
|
+
* The cautionary direction is the one that needed it, because this is
|
|
228
|
+
* the side with a dollar figure attached. Only when the number is an
|
|
229
|
+
* estimate: a caller who supplied their own counter has an
|
|
230
|
+
* authoritative prefix and hedging it would push them toward a check
|
|
231
|
+
* they have already done.
|
|
232
|
+
*/
|
|
233
|
+
nearMinimum:
|
|
234
|
+
count === estimateTokens &&
|
|
235
|
+
cache.stablePrefixTokens * (1 - ESTIMATE_ERROR_BAND_PCT / 100) < minTokens,
|
|
188
236
|
}),
|
|
189
237
|
estimatedMonthlyUsd: saving,
|
|
190
238
|
});
|
|
@@ -223,12 +271,49 @@ export function buildAdvisories(
|
|
|
223
271
|
});
|
|
224
272
|
}
|
|
225
273
|
|
|
226
|
-
|
|
227
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Stable content placed AFTER the first placeholder: never cached today, and
|
|
276
|
+
* cacheable if it moves in front.
|
|
277
|
+
*
|
|
278
|
+
* **The prefix it would produce has to clear the minimum, and it did not used
|
|
279
|
+
* to be checked.** That was a money figure in the flattering direction, which
|
|
280
|
+
* is the one fault this file exists to avoid. On a 306-token support prompt
|
|
281
|
+
* against Claude Opus 5's 512-token minimum, the best prefix a rearrangement
|
|
282
|
+
* can build is 302 — so nothing caches, and the advisory offered $48.67 a
|
|
283
|
+
* month that cannot be collected.
|
|
284
|
+
*
|
|
285
|
+
* Worse, it said so in the same report as `below-cache-minimum`, which was
|
|
286
|
+
* telling the reader caching would not work here at all. Two advisories
|
|
287
|
+
* contradicting each other, and the one with a dollar sign winning the
|
|
288
|
+
* argument.
|
|
289
|
+
*
|
|
290
|
+
* `reorderForCache` already refused these prompts for exactly this reason, so
|
|
291
|
+
* the tool's advice and its action disagreed: follow the advice, run
|
|
292
|
+
* `--reorder`, and watch nothing happen.
|
|
293
|
+
*/
|
|
294
|
+
/**
|
|
295
|
+
* The best prefix any rearrangement could build, compared strictly.
|
|
296
|
+
*
|
|
297
|
+
* **No band hedge here, and that was tried first.** Widening the comparison by
|
|
298
|
+
* ±10% — on the same reasoning that makes `below-cache-minimum` hedge near the
|
|
299
|
+
* line — opened a window between 466 and 512 tokens where this advisory
|
|
300
|
+
* offered a saving and `reorderForCache` refused to perform it. That is the
|
|
301
|
+
* fault being fixed, reintroduced one layer up, and a test caught it.
|
|
302
|
+
*
|
|
303
|
+
* The near-the-line case is already handled and in the right place:
|
|
304
|
+
* `below-cache-minimum` says the estimate is close to the threshold and names
|
|
305
|
+
* `--exact-tokens`. Settle the number and both this advisory and the command
|
|
306
|
+
* work from the same certainty. Two components disagreeing is worse than one
|
|
307
|
+
* of them being briefly quiet.
|
|
308
|
+
*/
|
|
309
|
+
const reorderedPrefix = cache.stablePrefixTokens + cache.staticTokensAfter;
|
|
310
|
+
const reachableAfterReorder = reorderedPrefix >= minTokens;
|
|
311
|
+
|
|
228
312
|
if (
|
|
229
313
|
cache.firstPlaceholder &&
|
|
230
314
|
cache.staticTokensAfter >= 200 &&
|
|
231
|
-
cache.staticTokensAfter >= tokensAfter * 0.3
|
|
315
|
+
cache.staticTokensAfter >= tokensAfter * 0.3 &&
|
|
316
|
+
reachableAfterReorder
|
|
232
317
|
) {
|
|
233
318
|
const movableShare = tokensAfter > 0 ? cache.staticTokensAfter / tokensAfter : 0;
|
|
234
319
|
const saving = monthlyInputUsd * movableShare * Math.max(0, 1 - factor);
|
|
@@ -239,6 +324,13 @@ export function buildAdvisories(
|
|
|
239
324
|
staticTokensAfter: cache.staticTokensAfter,
|
|
240
325
|
sharePct: Math.round(movableShare * 100),
|
|
241
326
|
placeholder: cache.firstPlaceholder,
|
|
327
|
+
/**
|
|
328
|
+
* Trazum can do this, and until now it told you to do it by hand.
|
|
329
|
+
* `reorderForCache` moves whole blocks, refuses any block carrying a
|
|
330
|
+
* backward reference, and refuses everything after one — so the command
|
|
331
|
+
* is the safe way to attempt what the prose was describing.
|
|
332
|
+
*/
|
|
333
|
+
command: 'trazum optimize <file> --reorder',
|
|
242
334
|
}),
|
|
243
335
|
estimatedMonthlyUsd: saving > 0 ? saving : null,
|
|
244
336
|
});
|
package/src/i18n/en.ts
CHANGED
|
@@ -91,9 +91,18 @@ export const en: CoreMessages = {
|
|
|
91
91
|
},
|
|
92
92
|
|
|
93
93
|
advisories: {
|
|
94
|
-
contextOverflow: ({ tokens, modelName, contextWindow }) => ({
|
|
95
|
-
title:
|
|
96
|
-
|
|
94
|
+
contextOverflow: ({ tokens, modelName, contextWindow, uncertain }) => ({
|
|
95
|
+
title: uncertain
|
|
96
|
+
? 'The prompt probably does not fit in the context window'
|
|
97
|
+
: 'The prompt does not fit in the context window',
|
|
98
|
+
detail: uncertain
|
|
99
|
+
? `The optimised prompt is ~${n(tokens)} tokens against ${modelName}'s ${n(contextWindow)}. That count is an estimate and it is close to the line, so the call will probably fail but might not — settle it with --exact-tokens before rewriting anything. The counting endpoint is free. If it does exceed the window, split the content or move to a model with a larger one.`
|
|
100
|
+
: `The optimised prompt is ~${n(tokens)} tokens and ${modelName} accepts ${n(contextWindow)}. The call will fail: split the content or move to a model with a larger window.`,
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
contextNearLimit: ({ tokens, modelName, contextWindow }) => ({
|
|
104
|
+
title: 'The prompt may not fit in the context window',
|
|
105
|
+
detail: `The optimised prompt is ~${n(tokens)} tokens against ${modelName}'s ${n(contextWindow)}, which fits — but that count is an estimate and its error range reaches past the window, so the real prompt may not. A call that exceeds the window fails outright rather than degrading, and nothing else here warns about it. Confirm with --exact-tokens; the counting endpoint is free.`,
|
|
97
106
|
}),
|
|
98
107
|
|
|
99
108
|
promptCaching: ({
|
|
@@ -106,6 +115,7 @@ export const en: CoreMessages = {
|
|
|
106
115
|
readPct,
|
|
107
116
|
writePct,
|
|
108
117
|
explicit,
|
|
118
|
+
nearMinimum,
|
|
109
119
|
}) => {
|
|
110
120
|
const scope = placeholder
|
|
111
121
|
? `The stable prefix — everything before the first placeholder ${placeholder} — is ~${n(prefixTokens)} of the prompt's ${n(totalTokens)} tokens, and clears ${modelName}'s ${n(minTokens)}-token cacheable minimum.`
|
|
@@ -113,9 +123,12 @@ export const en: CoreMessages = {
|
|
|
113
123
|
const how = explicit
|
|
114
124
|
? 'Put the cache marker at the end of the stable prefix: any byte that changes before the cut invalidates everything after it.'
|
|
115
125
|
: `${modelName} caches automatically above its minimum, so there is nothing to set — but the same rule applies: any byte that changes before the cut invalidates everything after it.`;
|
|
126
|
+
const hedge = nearMinimum
|
|
127
|
+
? ` One caveat on the figure: that prefix count is an estimate and it is close to the line, so the real one may be below the ${n(minTokens)}-token minimum — in which case nothing caches and this saving is not there. Settle it with --exact-tokens before budgeting from it. The counting endpoint is free.`
|
|
128
|
+
: '';
|
|
116
129
|
return {
|
|
117
130
|
title: 'Turn on prompt caching for the stable prefix',
|
|
118
|
-
detail: `${scope} At a ${hitRatePct}% hit rate, a cache read costs ${readPct}% of the input price and a write costs ${writePct}%. ${how}`,
|
|
131
|
+
detail: `${scope} At a ${hitRatePct}% hit rate, a cache read costs ${readPct}% of the input price and a write costs ${writePct}%. ${how}${hedge}`,
|
|
119
132
|
};
|
|
120
133
|
},
|
|
121
134
|
|
|
@@ -150,9 +163,9 @@ export const en: CoreMessages = {
|
|
|
150
163
|
};
|
|
151
164
|
},
|
|
152
165
|
|
|
153
|
-
cachePrefixReorder: ({ staticTokensAfter, sharePct, placeholder }) => ({
|
|
166
|
+
cachePrefixReorder: ({ staticTokensAfter, sharePct, placeholder, command }) => ({
|
|
154
167
|
title: 'Move the stable instructions ahead of the first placeholder',
|
|
155
|
-
detail: `About ~${n(staticTokensAfter)} tokens of stable content (${sharePct}% of the prompt) sit after the first variable placeholder ${placeholder}, so today they never get cached.
|
|
168
|
+
detail: `About ~${n(staticTokensAfter)} tokens of stable content (${sharePct}% of the prompt) sit after the first variable placeholder ${placeholder}, so today they never get cached. Fixed instructions and context first, placeholders last, and that content starts being read from cache at 10% of the price. Run \`${command}\` to attempt it: whole blocks only, and it refuses to move anything that refers back to earlier text. Read the diff — order carries meaning, and "summarise the text above" is nonsense in front of the text it points at.`,
|
|
156
169
|
}),
|
|
157
170
|
|
|
158
171
|
batchApi: () => ({
|
package/src/i18n/es.ts
CHANGED
|
@@ -91,9 +91,18 @@ export const es: CoreMessages = {
|
|
|
91
91
|
},
|
|
92
92
|
|
|
93
93
|
advisories: {
|
|
94
|
-
contextOverflow: ({ tokens, modelName, contextWindow }) => ({
|
|
95
|
-
title:
|
|
96
|
-
|
|
94
|
+
contextOverflow: ({ tokens, modelName, contextWindow, uncertain }) => ({
|
|
95
|
+
title: uncertain
|
|
96
|
+
? 'El prompt probablemente no cabe en la ventana de contexto'
|
|
97
|
+
: 'El prompt no cabe en la ventana de contexto',
|
|
98
|
+
detail: uncertain
|
|
99
|
+
? `El prompt optimizado ocupa ~${n(tokens)} tokens frente a los ${n(contextWindow)} de ${modelName}. Ese recuento es una estimación y está cerca del límite, así que la llamada fallará probablemente, pero puede que no —confírmalo con --exact-tokens antes de reescribir nada. El endpoint de conteo es gratis. Si de verdad se pasa, divide el contenido o cambia a un modelo con ventana mayor.`
|
|
100
|
+
: `El prompt optimizado ocupa ~${n(tokens)} tokens y ${modelName} admite ${n(contextWindow)}. La llamada fallará: divide el contenido o cambia a un modelo con ventana mayor.`,
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
contextNearLimit: ({ tokens, modelName, contextWindow }) => ({
|
|
104
|
+
title: 'El prompt puede no caber en la ventana de contexto',
|
|
105
|
+
detail: `El prompt optimizado ocupa ~${n(tokens)} tokens frente a los ${n(contextWindow)} de ${modelName}, así que cabe —pero ese recuento es una estimación y su margen de error se pasa de la ventana, así que el prompt real puede no caber. Una llamada que excede la ventana falla del todo en lugar de degradarse, y nada más aquí avisa de eso. Confírmalo con --exact-tokens; el endpoint de conteo es gratis.`,
|
|
97
106
|
}),
|
|
98
107
|
|
|
99
108
|
promptCaching: ({
|
|
@@ -106,6 +115,7 @@ export const es: CoreMessages = {
|
|
|
106
115
|
readPct,
|
|
107
116
|
writePct,
|
|
108
117
|
explicit,
|
|
118
|
+
nearMinimum,
|
|
109
119
|
}) => {
|
|
110
120
|
const scope = placeholder
|
|
111
121
|
? `El prefijo estable —lo anterior al primer marcador ${placeholder}— son ~${n(prefixTokens)} de los ${n(totalTokens)} tokens del prompt, y supera el mínimo cacheable de ${n(minTokens)} de ${modelName}.`
|
|
@@ -113,9 +123,12 @@ export const es: CoreMessages = {
|
|
|
113
123
|
const how = explicit
|
|
114
124
|
? 'Coloca el marcador de caché al final del prefijo estable: cualquier byte que cambie antes del corte invalida todo lo que va detrás.'
|
|
115
125
|
: `${modelName} cachea automáticamente por encima de su mínimo, así que no hay nada que activar; pero la regla es la misma: cualquier byte que cambie antes del corte invalida todo lo que va detrás.`;
|
|
126
|
+
const hedge = nearMinimum
|
|
127
|
+
? ` Un aviso sobre la cifra: ese recuento del prefijo es una estimación y está cerca del límite, así que el real puede quedar por debajo del mínimo de ${n(minTokens)} tokens —y entonces no se cachea nada y este ahorro no existe. Confírmalo con --exact-tokens antes de presupuestar sobre él. El endpoint de conteo es gratis.`
|
|
128
|
+
: '';
|
|
116
129
|
return {
|
|
117
130
|
title: 'Activa prompt caching en el prefijo estable',
|
|
118
|
-
detail: `${scope} Con una tasa de acierto del ${hitRatePct}%, la lectura de caché cuesta un ${readPct}% del precio de entrada y la escritura un ${writePct}%. ${how}`,
|
|
131
|
+
detail: `${scope} Con una tasa de acierto del ${hitRatePct}%, la lectura de caché cuesta un ${readPct}% del precio de entrada y la escritura un ${writePct}%. ${how}${hedge}`,
|
|
119
132
|
};
|
|
120
133
|
},
|
|
121
134
|
|
|
@@ -150,9 +163,9 @@ export const es: CoreMessages = {
|
|
|
150
163
|
};
|
|
151
164
|
},
|
|
152
165
|
|
|
153
|
-
cachePrefixReorder: ({ staticTokensAfter, sharePct, placeholder }) => ({
|
|
166
|
+
cachePrefixReorder: ({ staticTokensAfter, sharePct, placeholder, command }) => ({
|
|
154
167
|
title: 'Mueve las instrucciones estables antes del primer marcador',
|
|
155
|
-
detail: `Unos ~${n(staticTokensAfter)} tokens de contenido estable (el ${sharePct}% del prompt) están después del primer marcador variable ${placeholder}, así que hoy no se cachean nunca.
|
|
168
|
+
detail: `Unos ~${n(staticTokensAfter)} tokens de contenido estable (el ${sharePct}% del prompt) están después del primer marcador variable ${placeholder}, así que hoy no se cachean nunca. Instrucciones y contexto fijos primero, marcadores al final, y ese contenido empieza a leerse de caché al 10% del precio. Ejecuta \`${command}\` para intentarlo: solo mueve bloques completos y se niega a mover cualquiera que se refiera a texto anterior. Lee el diff —el orden significa algo, y «resume el texto de arriba» no tiene sentido delante del texto al que apunta.`,
|
|
156
169
|
}),
|
|
157
170
|
|
|
158
171
|
batchApi: () => ({
|
package/src/i18n/types.ts
CHANGED
|
@@ -51,6 +51,17 @@ export interface RuleCopy {
|
|
|
51
51
|
// --------------------------------------------------------------------------
|
|
52
52
|
|
|
53
53
|
export interface ContextOverflowParams {
|
|
54
|
+
/**
|
|
55
|
+
* The count is an estimate and its band reaches back under the window, so
|
|
56
|
+
* "the call will fail" is a prediction rather than a fact.
|
|
57
|
+
*/
|
|
58
|
+
uncertain: boolean;
|
|
59
|
+
tokens: number;
|
|
60
|
+
modelName: string;
|
|
61
|
+
contextWindow: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ContextNearLimitParams {
|
|
54
65
|
tokens: number;
|
|
55
66
|
modelName: string;
|
|
56
67
|
contextWindow: number;
|
|
@@ -60,6 +71,20 @@ export interface PromptCachingParams {
|
|
|
60
71
|
/** First template placeholder, or `null` when the prompt has none. */
|
|
61
72
|
placeholder: string | null;
|
|
62
73
|
prefixTokens: number;
|
|
74
|
+
/**
|
|
75
|
+
* The prefix is an estimate and the band reaches below the minimum, so the
|
|
76
|
+
* saving may not be collectable at all.
|
|
77
|
+
*
|
|
78
|
+
* The mirror of `BelowCacheMinimumParams.couldReachMinimum`, and the asymmetry
|
|
79
|
+
* was a real gap: that one hedged an estimate landing just *under* a hard
|
|
80
|
+
* threshold, while this one promised money on an estimate landing just *over*
|
|
81
|
+
* it. With a ±10% band an estimated 528-token prefix can truly be 475, in which
|
|
82
|
+
* case nothing caches and the figure beside this advisory is uncollectable.
|
|
83
|
+
*
|
|
84
|
+
* The cautionary direction matters more than the encouraging one, because this
|
|
85
|
+
* is the side with a dollar sign attached.
|
|
86
|
+
*/
|
|
87
|
+
nearMinimum: boolean;
|
|
63
88
|
totalTokens: number;
|
|
64
89
|
minTokens: number;
|
|
65
90
|
modelName: string;
|
|
@@ -93,7 +118,7 @@ export interface BelowCacheMinimumParams {
|
|
|
93
118
|
* count could be above it.
|
|
94
119
|
*
|
|
95
120
|
* Without this the advisory asserts "caching will not work here" from a number
|
|
96
|
-
* measured to ±
|
|
121
|
+
* measured to ±10%, and on a prefix near the threshold that is not an imprecise
|
|
97
122
|
* figure — it is wrong advice, and it costs the reader the largest saving
|
|
98
123
|
* Trazum offers.
|
|
99
124
|
*/
|
|
@@ -104,6 +129,8 @@ export interface CachePrefixReorderParams {
|
|
|
104
129
|
staticTokensAfter: number;
|
|
105
130
|
sharePct: number;
|
|
106
131
|
placeholder: string;
|
|
132
|
+
/** The command that attempts it, because Trazum can do this itself. */
|
|
133
|
+
command: string;
|
|
107
134
|
}
|
|
108
135
|
|
|
109
136
|
export interface ModelDowngradeParams {
|
|
@@ -193,6 +220,7 @@ export interface CoreMessages {
|
|
|
193
220
|
suggest: SuggestMessages;
|
|
194
221
|
advisories: {
|
|
195
222
|
contextOverflow(p: ContextOverflowParams): LocalizedMessage;
|
|
223
|
+
contextNearLimit(p: ContextNearLimitParams): LocalizedMessage;
|
|
196
224
|
promptCaching(p: PromptCachingParams): LocalizedMessage;
|
|
197
225
|
promptCachingNotWorthIt(): LocalizedMessage;
|
|
198
226
|
belowCacheMinimum(p: BelowCacheMinimumParams): LocalizedMessage;
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export { ESTIMATE_ERROR_BAND_PCT, estimateTokens, countTokensAnthropic } from './tokenizer.js';
|
|
3
|
+
export {
|
|
4
|
+
UNLABELLED,
|
|
5
|
+
cacheHitRate,
|
|
6
|
+
parseUsageLine,
|
|
7
|
+
profileUsage,
|
|
8
|
+
sharesOf,
|
|
9
|
+
} from './usage.js';
|
|
10
|
+
export type {
|
|
11
|
+
UsageProfileOptions,
|
|
12
|
+
UsageBreakdown,
|
|
13
|
+
UsageProfileReport,
|
|
14
|
+
UsageRecord,
|
|
15
|
+
UsageShares,
|
|
16
|
+
} from './usage.js';
|
|
3
17
|
export { DETECTABLE_LANGUAGES, detectTextLanguage } from './language.js';
|
|
4
18
|
export { countSentences, profilePrompt } from './profile.js';
|
|
5
19
|
export { PHRASE_LANGUAGES } from './phrases.js';
|