@trazum/core 1.8.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 +28 -8
- package/dist/advisories.d.ts.map +1 -1
- package/dist/advisories.js +105 -5
- package/dist/advisories.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +22 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +22 -8
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +37 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/language.d.ts +38 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +109 -0
- package/dist/language.js.map +1 -0
- package/dist/tokenizer.d.ts +41 -1
- package/dist/tokenizer.d.ts.map +1 -1
- package/dist/tokenizer.js +160 -7
- 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 +108 -5
- package/src/i18n/en.ts +23 -6
- package/src/i18n/es.ts +23 -6
- package/src/i18n/types.ts +38 -0
- package/src/index.ts +16 -1
- package/src/language.ts +112 -0
- package/src/tokenizer.ts +166 -7
- package/src/types.ts +1 -0
- package/src/usage.ts +479 -0
package/src/advisories.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { PricingCatalogue } from './pricing.js';
|
|
|
7
7
|
import { formatUsd } from './savings.js';
|
|
8
8
|
import { analyzeExamples, findContradictions, findMovableSchema,
|
|
9
9
|
findRestatedFormat } from './structure.js';
|
|
10
|
-
import { estimateTokens } from './tokenizer.js';
|
|
10
|
+
import { ESTIMATE_ERROR_BAND_PCT, estimateTokens } from './tokenizer.js';
|
|
11
11
|
import type { Advisory, ModelPricing, TokenCounter, UsageProfile } from './types.js';
|
|
12
12
|
|
|
13
13
|
function countSignals(haystack: string, signals: readonly string[]): number {
|
|
@@ -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
|
});
|
|
@@ -207,17 +255,65 @@ export function buildAdvisories(
|
|
|
207
255
|
prefixTokens: cache.stablePrefixTokens,
|
|
208
256
|
totalTokens: tokensAfter,
|
|
209
257
|
mentionLowerMinimum: minTokens > 512,
|
|
258
|
+
/**
|
|
259
|
+
* Only when the number is an estimate and near the line.
|
|
260
|
+
*
|
|
261
|
+
* `count` defaults to `estimateTokens`; a caller who supplied their own
|
|
262
|
+
* counter — `--exact-tokens`, or the official endpoint — gets an
|
|
263
|
+
* authoritative number and no hedge, because hedging a measured figure
|
|
264
|
+
* is its own kind of dishonesty.
|
|
265
|
+
*/
|
|
266
|
+
couldReachMinimum:
|
|
267
|
+
count === estimateTokens &&
|
|
268
|
+
cache.stablePrefixTokens * (1 + ESTIMATE_ERROR_BAND_PCT / 100) >= minTokens,
|
|
210
269
|
}),
|
|
211
270
|
estimatedMonthlyUsd: null,
|
|
212
271
|
});
|
|
213
272
|
}
|
|
214
273
|
|
|
215
|
-
|
|
216
|
-
|
|
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
|
+
|
|
217
312
|
if (
|
|
218
313
|
cache.firstPlaceholder &&
|
|
219
314
|
cache.staticTokensAfter >= 200 &&
|
|
220
|
-
cache.staticTokensAfter >= tokensAfter * 0.3
|
|
315
|
+
cache.staticTokensAfter >= tokensAfter * 0.3 &&
|
|
316
|
+
reachableAfterReorder
|
|
221
317
|
) {
|
|
222
318
|
const movableShare = tokensAfter > 0 ? cache.staticTokensAfter / tokensAfter : 0;
|
|
223
319
|
const saving = monthlyInputUsd * movableShare * Math.max(0, 1 - factor);
|
|
@@ -228,6 +324,13 @@ export function buildAdvisories(
|
|
|
228
324
|
staticTokensAfter: cache.staticTokensAfter,
|
|
229
325
|
sharePct: Math.round(movableShare * 100),
|
|
230
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',
|
|
231
334
|
}),
|
|
232
335
|
estimatedMonthlyUsd: saving > 0 ? saving : null,
|
|
233
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
|
|
|
@@ -132,6 +145,7 @@ export const en: CoreMessages = {
|
|
|
132
145
|
prefixTokens,
|
|
133
146
|
totalTokens,
|
|
134
147
|
mentionLowerMinimum,
|
|
148
|
+
couldReachMinimum,
|
|
135
149
|
}) => {
|
|
136
150
|
const reason = placeholder
|
|
137
151
|
? `here the first variable placeholder (${placeholder}) shows up at ~${n(prefixTokens)} tokens and only what precedes it can be cached`
|
|
@@ -142,13 +156,16 @@ export const en: CoreMessages = {
|
|
|
142
156
|
`${modelName} needs at least ${n(minTokens)} prefix tokens to cache; ${reason}. Setting cache_control will not error, it simply will not cache.` +
|
|
143
157
|
(mentionLowerMinimum
|
|
144
158
|
? ' Claude Opus 5 lowers that minimum to 512 tokens, so short prompts that miss here would cache there.'
|
|
159
|
+
: '') +
|
|
160
|
+
(couldReachMinimum
|
|
161
|
+
? ' That prefix count is an estimate and it is close to the line, so the real one may already be above it — check with --exact-tokens before deciding this is not available to you. The counting endpoint is free.'
|
|
145
162
|
: ''),
|
|
146
163
|
};
|
|
147
164
|
},
|
|
148
165
|
|
|
149
|
-
cachePrefixReorder: ({ staticTokensAfter, sharePct, placeholder }) => ({
|
|
166
|
+
cachePrefixReorder: ({ staticTokensAfter, sharePct, placeholder, command }) => ({
|
|
150
167
|
title: 'Move the stable instructions ahead of the first placeholder',
|
|
151
|
-
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.`,
|
|
152
169
|
}),
|
|
153
170
|
|
|
154
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
|
|
|
@@ -132,6 +145,7 @@ export const es: CoreMessages = {
|
|
|
132
145
|
prefixTokens,
|
|
133
146
|
totalTokens,
|
|
134
147
|
mentionLowerMinimum,
|
|
148
|
+
couldReachMinimum,
|
|
135
149
|
}) => {
|
|
136
150
|
const reason = placeholder
|
|
137
151
|
? `aquí el primer marcador variable (${placeholder}) aparece a los ~${n(prefixTokens)} tokens y solo lo anterior puede cachearse`
|
|
@@ -142,13 +156,16 @@ export const es: CoreMessages = {
|
|
|
142
156
|
`${modelName} necesita al menos ${n(minTokens)} tokens de prefijo para cachear; ${reason}. Marcar cache_control no dará error, simplemente no cacheará.` +
|
|
143
157
|
(mentionLowerMinimum
|
|
144
158
|
? ' Claude Opus 5 baja ese mínimo a 512 tokens, así que prompts cortos que aquí no cachean, allí sí.'
|
|
159
|
+
: '') +
|
|
160
|
+
(couldReachMinimum
|
|
161
|
+
? ' Ese recuento del prefijo es una estimación y está cerca del límite, así que el real puede estar ya por encima: compruébalo con --exact-tokens antes de dar esto por perdido. El endpoint de conteo es gratis.'
|
|
145
162
|
: ''),
|
|
146
163
|
};
|
|
147
164
|
},
|
|
148
165
|
|
|
149
|
-
cachePrefixReorder: ({ staticTokensAfter, sharePct, placeholder }) => ({
|
|
166
|
+
cachePrefixReorder: ({ staticTokensAfter, sharePct, placeholder, command }) => ({
|
|
150
167
|
title: 'Mueve las instrucciones estables antes del primer marcador',
|
|
151
|
-
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.`,
|
|
152
169
|
}),
|
|
153
170
|
|
|
154
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;
|
|
@@ -88,12 +113,24 @@ export interface BelowCacheMinimumParams {
|
|
|
88
113
|
totalTokens: number;
|
|
89
114
|
/** Whether to point at Claude Opus 5's lower 512-token minimum. */
|
|
90
115
|
mentionLowerMinimum: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* The prefix is *estimated* and close enough to the minimum that the real
|
|
118
|
+
* count could be above it.
|
|
119
|
+
*
|
|
120
|
+
* Without this the advisory asserts "caching will not work here" from a number
|
|
121
|
+
* measured to ±10%, and on a prefix near the threshold that is not an imprecise
|
|
122
|
+
* figure — it is wrong advice, and it costs the reader the largest saving
|
|
123
|
+
* Trazum offers.
|
|
124
|
+
*/
|
|
125
|
+
couldReachMinimum: boolean;
|
|
91
126
|
}
|
|
92
127
|
|
|
93
128
|
export interface CachePrefixReorderParams {
|
|
94
129
|
staticTokensAfter: number;
|
|
95
130
|
sharePct: number;
|
|
96
131
|
placeholder: string;
|
|
132
|
+
/** The command that attempts it, because Trazum can do this itself. */
|
|
133
|
+
command: string;
|
|
97
134
|
}
|
|
98
135
|
|
|
99
136
|
export interface ModelDowngradeParams {
|
|
@@ -183,6 +220,7 @@ export interface CoreMessages {
|
|
|
183
220
|
suggest: SuggestMessages;
|
|
184
221
|
advisories: {
|
|
185
222
|
contextOverflow(p: ContextOverflowParams): LocalizedMessage;
|
|
223
|
+
contextNearLimit(p: ContextNearLimitParams): LocalizedMessage;
|
|
186
224
|
promptCaching(p: PromptCachingParams): LocalizedMessage;
|
|
187
225
|
promptCachingNotWorthIt(): LocalizedMessage;
|
|
188
226
|
belowCacheMinimum(p: BelowCacheMinimumParams): LocalizedMessage;
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
|
-
export { estimateTokens, countTokensAnthropic } from './tokenizer.js';
|
|
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';
|
|
17
|
+
export { DETECTABLE_LANGUAGES, detectTextLanguage } from './language.js';
|
|
3
18
|
export { countSentences, profilePrompt } from './profile.js';
|
|
4
19
|
export { PHRASE_LANGUAGES } from './phrases.js';
|
|
5
20
|
export { toPromptfoo } from './promptfoo.js';
|
package/src/language.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which language a prompt is written in, to the extent it can be told cheaply.
|
|
3
|
+
*
|
|
4
|
+
* **Why the estimator needs this at all.** Measured against the counting
|
|
5
|
+
* endpoint, `estimateTokens` is accurate on English (+1.0%) and badly low on every
|
|
6
|
+
* other Latin language: German -37.3%, Spanish -22.9%, French -15.1%. Characters
|
|
7
|
+
* per token says why — English 3.44, French 2.66, Spanish 2.53, German 2.02 —
|
|
8
|
+
* while the estimator applied one divisor to all of them. Non-English text is
|
|
9
|
+
* thinner in the merge table, so the same number of characters costs more tokens.
|
|
10
|
+
*
|
|
11
|
+
* **Accents are not the signal.** That was the first hypothesis and it was tested
|
|
12
|
+
* and killed: a Spanish sample with zero accented characters comes out at -22.9%,
|
|
13
|
+
* against -22.1% for accented Spanish. Diacritics correlate with non-English text
|
|
14
|
+
* in a corpus and not in a prompt, and weighting them moved the figure by three
|
|
15
|
+
* points. What separates these languages is which words they are made of.
|
|
16
|
+
*
|
|
17
|
+
* So this counts function words — the shortest, commonest, most language-specific
|
|
18
|
+
* tokens there are. `the of and to` against `der die und ist` against
|
|
19
|
+
* `que de la en`.
|
|
20
|
+
*
|
|
21
|
+
* **It answers `null` when unsure, and that is the important part.** A prompt is
|
|
22
|
+
* not an essay: it can be three lines, or English instructions wrapped around a
|
|
23
|
+
* Spanish example, or a JSON schema with no prose at all. Guessing on those would
|
|
24
|
+
* apply a language's divisor to text that is not in that language, which is how a
|
|
25
|
+
* fix for one case becomes a regression for four. `null` means "use the default",
|
|
26
|
+
* and the default is the English-calibrated behaviour this has always had.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Function words per language, lower-cased, matched whole.
|
|
31
|
+
*
|
|
32
|
+
* Chosen for frequency and for *not overlapping*, which matters more: `de` is
|
|
33
|
+
* frequent in Spanish, French and Portuguese, so it earns nothing and is a
|
|
34
|
+
* liability. Each list is words that are common in its own language and rare in
|
|
35
|
+
* the others, which is what lets a small count decide.
|
|
36
|
+
*
|
|
37
|
+
* `en` is included even though it is the default, because the margin rule needs to
|
|
38
|
+
* know when English *won*: a Spanish prompt quoting an English sentence should not
|
|
39
|
+
* become Spanish on two hits.
|
|
40
|
+
*/
|
|
41
|
+
const FUNCTION_WORDS: Readonly<Record<string, readonly string[]>> = {
|
|
42
|
+
en: ['the', 'and', 'of', 'to', 'is', 'that', 'with', 'for', 'you', 'this', 'are', 'not'],
|
|
43
|
+
es: ['que', 'los', 'las', 'del', 'una', 'para', 'con', 'como', 'pero', 'sus', 'este', 'siempre'],
|
|
44
|
+
fr: ['les', 'des', 'une', 'dans', 'pour', 'avec', 'vous', 'est', 'sur', 'aux', 'cette', 'toujours'],
|
|
45
|
+
de: ['der', 'die', 'das', 'und', 'nicht', 'sie', 'ist', 'mit', 'auf', 'einen', 'einer', 'immer'],
|
|
46
|
+
pt: ['que', 'dos', 'das', 'uma', 'para', 'com', 'como', 'não', 'seu', 'este', 'pelo', 'sempre'],
|
|
47
|
+
/**
|
|
48
|
+
* Rebuilt once, because half of it was Spanish. `per con del una sempre` are as
|
|
49
|
+
* common in Spanish as in Italian, so they earned nothing and cost the margin
|
|
50
|
+
* rule its answer: an Italian code-review prompt scored a tie and came back
|
|
51
|
+
* `null`, fell through to the English divisor, and measured -21.9%. These are
|
|
52
|
+
* words Italian has and Spanish does not.
|
|
53
|
+
*/
|
|
54
|
+
it: ['il', 'della', 'nella', 'nel', 'che', 'quando', 'senza', 'gli', 'delle', 'degli', 'più', 'anche'],
|
|
55
|
+
nl: ['het', 'een', 'van', 'niet', 'zijn', 'met', 'voor', 'dat', 'aan', 'deze', 'wordt', 'altijd'],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Languages this can name. Exported so callers can key a table off it. */
|
|
59
|
+
export const DETECTABLE_LANGUAGES: readonly string[] = Object.keys(FUNCTION_WORDS);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* How many hits the winner needs, absolutely and relative to the runner-up.
|
|
63
|
+
*
|
|
64
|
+
* Both bars exist for the same reason and neither is enough alone. The absolute
|
|
65
|
+
* one stops a three-line prompt being classified on a single word. The ratio stops
|
|
66
|
+
* a document that is genuinely mixed — English instructions around a Spanish
|
|
67
|
+
* example — from being called whichever language happened to appear once more.
|
|
68
|
+
*
|
|
69
|
+
* Deliberately cautious in the direction of `null`. A wrong language costs
|
|
70
|
+
* accuracy on text that was fine before; `null` costs only the improvement.
|
|
71
|
+
*/
|
|
72
|
+
const MIN_HITS = 4;
|
|
73
|
+
const MIN_RATIO = 1.6;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The language of `text`, or `null` when no answer is safe.
|
|
77
|
+
*
|
|
78
|
+
* Case-insensitive, whole words only, and it stops reading after a bounded
|
|
79
|
+
* prefix: a prompt can be a megabyte, the answer does not get better after a few
|
|
80
|
+
* thousand words, and this runs inside `estimateTokens` on every call.
|
|
81
|
+
*/
|
|
82
|
+
export function detectTextLanguage(text: string): string | null {
|
|
83
|
+
if (!text) return null;
|
|
84
|
+
|
|
85
|
+
// Bounded so this stays cheap on a large prompt. Function words are frequent
|
|
86
|
+
// enough that a prefix this size decides anything a whole document would.
|
|
87
|
+
const sample = text.length > 20_000 ? text.slice(0, 20_000) : text;
|
|
88
|
+
|
|
89
|
+
const words = sample.toLowerCase().match(/[\p{L}]+/gu);
|
|
90
|
+
if (!words || words.length < MIN_HITS * 2) return null;
|
|
91
|
+
|
|
92
|
+
const seen = new Set(words);
|
|
93
|
+
const scores: Array<[string, number]> = [];
|
|
94
|
+
for (const [language, list] of Object.entries(FUNCTION_WORDS)) {
|
|
95
|
+
let hits = 0;
|
|
96
|
+
// Distinct words rather than occurrences: a prompt that repeats "the" forty
|
|
97
|
+
// times is not more English than one that uses forty different English words,
|
|
98
|
+
// and counting occurrences lets one hammered word decide.
|
|
99
|
+
for (const word of list) if (seen.has(word)) hits++;
|
|
100
|
+
scores.push([language, hits]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
scores.sort((a, b) => b[1] - a[1]);
|
|
104
|
+
const [best, bestHits] = scores[0]!;
|
|
105
|
+
const runnerUp = scores[1]?.[1] ?? 0;
|
|
106
|
+
|
|
107
|
+
if (bestHits < MIN_HITS) return null;
|
|
108
|
+
// A runner-up of zero is a clear win; guard the division rather than special-case.
|
|
109
|
+
if (runnerUp > 0 && bestHits / runnerUp < MIN_RATIO) return null;
|
|
110
|
+
|
|
111
|
+
return best;
|
|
112
|
+
}
|