@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/tokenizer.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is NOT a real tokenizer: it is a heuristic calibrated per character
|
|
5
5
|
* class. It is built to keep the typical error on ordinary text
|
|
6
|
-
* (English/Spanish, markdown, code) inside ±
|
|
6
|
+
* (English/Spanish, markdown, code) inside ±10%, which is plenty for comparing
|
|
7
7
|
* two versions of the same prompt — but do NOT bill anyone from it.
|
|
8
8
|
*
|
|
9
9
|
* **That band is a design target that has not been measured.** It is printed on
|
|
@@ -21,12 +21,140 @@
|
|
|
21
21
|
* endpoint, which is free) or pass your own `TokenCounter`.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { detectTextLanguage } from './language.js';
|
|
24
25
|
import { SAFE_FETCH_INIT, checkedEndpoint } from './net.js';
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The error band this estimator is published under, as a percentage.
|
|
29
|
+
*
|
|
30
|
+
* **Measured, not chosen, and it has moved three times.** It was `15` for eight
|
|
31
|
+
* releases as a design target nobody had checked; the first measurement found two
|
|
32
|
+
* of eight samples outside it and it went to `25`; fixing the digit divisor and
|
|
33
|
+
* calibrating per language brought it back to `15`. This is the fourth value and
|
|
34
|
+
* the first one that is comfortably above what the corpus actually shows.
|
|
35
|
+
*
|
|
36
|
+
* ```
|
|
37
|
+
* worst measured error 6.4% (code-heavy, which nothing is fitted to)
|
|
38
|
+
* published band 10%
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* **The margin is deliberate and it is not slack.** 6.4 rounded up is 7, and
|
|
42
|
+
* publishing 7 would be a tighter claim than twenty-one samples across six text
|
|
43
|
+
* types can support: the corpus has no Korean, no Arabic, no Cyrillic prose, no
|
|
44
|
+
* mixed-script document, and a seventh text type could easily land at eight. A
|
|
45
|
+
* band that becomes false the first time somebody measures something new is the
|
|
46
|
+
* exact fault this whole exercise was fixing. Overstating the uncertainty is the
|
|
47
|
+
* safe direction for a tool that reports money.
|
|
48
|
+
*
|
|
49
|
+
* What earned the drop from 15 was **splitting kana from han**. Every CJK
|
|
50
|
+
* character was charged one token, which put Japanese at +11.2% — the worst error
|
|
51
|
+
* anywhere in the corpus — while Chinese sat at −3.2% under the same rule. Kana
|
|
52
|
+
* measure 0.75 tokens per character and han 1.05, and that pair takes the two
|
|
53
|
+
* samples to −1.5% and +1.3%. See `KANA_TOKENS_PER_CHAR`.
|
|
54
|
+
*
|
|
55
|
+
* **Which samples the band rests on matters more than the number.** Eight of the
|
|
56
|
+
* twenty-one had a constant fitted to them — seven Latin divisors and the digit
|
|
57
|
+
* divisor — so their residuals are optimistic by construction. The two worst
|
|
58
|
+
* errors in the corpus, `code-heavy` at 6.4% and `punctuation-heavy` at 5.7%, are
|
|
59
|
+
* fitted to nothing at all, and they are what sets this figure.
|
|
60
|
+
*
|
|
61
|
+
* Exported so every report, README and tool description reads the same number. It
|
|
62
|
+
* was a literal in twenty-four files before this, with the only machine-readable
|
|
63
|
+
* copy in a test, and `token-band.test.js` now fails any file that states a
|
|
64
|
+
* different one.
|
|
65
|
+
*/
|
|
66
|
+
export const ESTIMATE_ERROR_BAND_PCT = 10;
|
|
67
|
+
|
|
26
68
|
const CJK = /[-ヿ㐀-䶿一-鿿가-]/;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Kana, separated from the rest of CJK because they do not cost the same.
|
|
72
|
+
*
|
|
73
|
+
* **This was the largest error left in the corpus.** Every CJK character was
|
|
74
|
+
* charged one token, and measured against the counting endpoint that put Japanese
|
|
75
|
+
* at **+11.2%** — the worst figure anywhere in twenty-one samples — while Chinese
|
|
76
|
+
* came out at −3.2% under the identical rule. One constant cannot be right for
|
|
77
|
+
* both, and the reason is visible in the samples: the Japanese one is 58% kana and
|
|
78
|
+
* the Chinese one is 0%.
|
|
79
|
+
*
|
|
80
|
+
* Kana are a small syllabary that appears in every sentence, so the merge table
|
|
81
|
+
* covers runs of them and several characters share a token. Han are tens of
|
|
82
|
+
* thousands of rare characters; a merge table cannot cover them and they cost
|
|
83
|
+
* about one each, sometimes more.
|
|
84
|
+
*
|
|
85
|
+
* The signal needs no detector. A character is kana or it is not, and the two
|
|
86
|
+
* samples separate perfectly — 58.3% against 0.00% — so this is a property of the
|
|
87
|
+
* character rather than a guess about the document. That is the difference between
|
|
88
|
+
* this and `language.ts`, which has to decide and is allowed to refuse.
|
|
89
|
+
*/
|
|
90
|
+
const KANA = /[-ヿ]/;
|
|
27
91
|
const LETTER = /[A-Za-zÀ-ɏͰ-ϿЀ-ӿ]/;
|
|
28
92
|
const DIGIT = /[0-9]/;
|
|
29
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Characters per token, per language.
|
|
96
|
+
*
|
|
97
|
+
* Measured, one entry at a time, against `test/fixtures/token-ground-truth.json`.
|
|
98
|
+
* English keeps 4 because it measures +1.0% there; the others are lower because
|
|
99
|
+
* they are thinner in the merge table and cost more tokens for the same text.
|
|
100
|
+
*
|
|
101
|
+
* **Every one of these now has a held-out test.** Each language was calibrated on
|
|
102
|
+
* support prompts and then measured again on a code-review prompt — a different
|
|
103
|
+
* register, different vocabulary, different length — and the divisors held:
|
|
104
|
+
* English +1.0% then +0.4%, German -9.2% then -8.5%, French -1.2% then -5.8%,
|
|
105
|
+
* Spanish -6.2% then -9.7% under the previous values. That is the evidence that
|
|
106
|
+
* these fit a language rather than a template, and it is why the band can rest on
|
|
107
|
+
* them now.
|
|
108
|
+
*
|
|
109
|
+
* `en` stays at a round 4 rather than the 4.05 the search prefers: a hundredth of
|
|
110
|
+
* a divisor is precision the twenty-one samples cannot support, and it changes no
|
|
111
|
+
* estimate.
|
|
112
|
+
*
|
|
113
|
+
* A language absent from this table falls through to `DEFAULT_DIVISOR`, which is
|
|
114
|
+
* the English number and the behaviour this estimator has always had. Adding a
|
|
115
|
+
* language means measuring it, not guessing it.
|
|
116
|
+
*/
|
|
117
|
+
const DIVISOR_BY_LANGUAGE: Readonly<Record<string, number>> = {
|
|
118
|
+
en: 4,
|
|
119
|
+
es: 2.8,
|
|
120
|
+
fr: 3.05,
|
|
121
|
+
de: 2.25,
|
|
122
|
+
it: 2.8,
|
|
123
|
+
pt: 3.05,
|
|
124
|
+
nl: 2.65,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Tokens per character for the two halves of CJK, measured.
|
|
129
|
+
*
|
|
130
|
+
* `0.75` and `1.05` come from a search over the two CJK samples in
|
|
131
|
+
* `test/fixtures/token-ground-truth.json`, and they take that pair from
|
|
132
|
+
* +11.2% / −3.2% to −1.5% / +1.3%.
|
|
133
|
+
*
|
|
134
|
+
* **Two samples fitted two constants, so those residuals are in-sample and
|
|
135
|
+
* optimistic by construction** — the same caveat the Latin divisors carry, stated
|
|
136
|
+
* for the same reason. What makes them worth having anyway is the size of the
|
|
137
|
+
* error they replace and the fact that they move in opposite directions: a single
|
|
138
|
+
* constant could not have been within four points of both, whatever it was set to.
|
|
139
|
+
* The honest test is a third CJK sample, and the corpus grows one at a time.
|
|
140
|
+
*
|
|
141
|
+
* Hangul keeps the old cost of 1, because nothing here measures Korean. Guessing
|
|
142
|
+
* it from Japanese would be inventing a figure — the two scripts have nothing in
|
|
143
|
+
* common that would make one predict the other.
|
|
144
|
+
*/
|
|
145
|
+
const KANA_TOKENS_PER_CHAR = 0.75;
|
|
146
|
+
const HAN_TOKENS_PER_CHAR = 1.05;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* What a prompt gets when the language could not be told.
|
|
150
|
+
*
|
|
151
|
+
* The English value on purpose. An unknown-language prompt is most often English
|
|
152
|
+
* or code, and lowering this would inflate every estimate that the detector
|
|
153
|
+
* declined to classify — a change that helps nothing measured and hurts the two
|
|
154
|
+
* samples the estimator gets right.
|
|
155
|
+
*/
|
|
156
|
+
const DEFAULT_DIVISOR = 4;
|
|
157
|
+
|
|
30
158
|
/** Effective word length: non-ASCII characters split into more tokens. */
|
|
31
159
|
function effectiveLength(word: string): number {
|
|
32
160
|
let len = 0;
|
|
@@ -49,7 +177,17 @@ function effectiveLength(word: string): number {
|
|
|
49
177
|
export function estimateTokens(text: string): number {
|
|
50
178
|
if (!text) return 0;
|
|
51
179
|
|
|
180
|
+
// Detected once for the whole text, not per word. A prompt is one document; a
|
|
181
|
+
// per-word guess would be noise, and this runs on every call.
|
|
182
|
+
const language = detectTextLanguage(text);
|
|
183
|
+
const divisor = (language === null ? undefined : DIVISOR_BY_LANGUAGE[language]) ?? DEFAULT_DIVISOR;
|
|
184
|
+
|
|
52
185
|
let total = 0;
|
|
186
|
+
/**
|
|
187
|
+
* CJK is summed separately because it is the one class counted in fractions of
|
|
188
|
+
* a token. Everything else is whole tokens by the character class it belongs to.
|
|
189
|
+
*/
|
|
190
|
+
let cjkTokens = 0;
|
|
53
191
|
let i = 0;
|
|
54
192
|
const chars = Array.from(text);
|
|
55
193
|
|
|
@@ -72,12 +210,19 @@ export function estimateTokens(text: string): number {
|
|
|
72
210
|
}
|
|
73
211
|
|
|
74
212
|
if (CJK.test(ch)) {
|
|
75
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Accumulated as a fraction and rounded once, at the end.
|
|
215
|
+
*
|
|
216
|
+
* Rounding up per run was the first attempt and it was wrong by five
|
|
217
|
+
* points. Ordinary Japanese alternates kana and han inside every sentence,
|
|
218
|
+
* so the runs are short and there are many of them — and a `Math.ceil` per
|
|
219
|
+
* run charges most of a token for each boundary. That is an artefact of
|
|
220
|
+
* where the loop happens to break, not of what the text costs.
|
|
221
|
+
*/
|
|
76
222
|
while (i < chars.length && CJK.test(chars[i]!)) {
|
|
77
|
-
|
|
223
|
+
cjkTokens += KANA.test(chars[i]!) ? KANA_TOKENS_PER_CHAR : HAN_TOKENS_PER_CHAR;
|
|
78
224
|
i++;
|
|
79
225
|
}
|
|
80
|
-
total += n;
|
|
81
226
|
continue;
|
|
82
227
|
}
|
|
83
228
|
|
|
@@ -87,7 +232,7 @@ export function estimateTokens(text: string): number {
|
|
|
87
232
|
word += chars[i]!;
|
|
88
233
|
i++;
|
|
89
234
|
}
|
|
90
|
-
total += Math.max(1, Math.ceil(effectiveLength(word) /
|
|
235
|
+
total += Math.max(1, Math.ceil(effectiveLength(word) / divisor));
|
|
91
236
|
continue;
|
|
92
237
|
}
|
|
93
238
|
|
|
@@ -97,7 +242,20 @@ export function estimateTokens(text: string): number {
|
|
|
97
242
|
n++;
|
|
98
243
|
i++;
|
|
99
244
|
}
|
|
100
|
-
|
|
245
|
+
/**
|
|
246
|
+
* 1.5 digits per token, not 3.
|
|
247
|
+
*
|
|
248
|
+
* The 3 was a guess and it was the single worst constant in this file:
|
|
249
|
+
* measured against the counting endpoint, the numeric-heavy sample came out
|
|
250
|
+
* **30.6% under** — by far the largest error in the corpus. Claude's
|
|
251
|
+
* tokenizer splits long digit runs far more finely than prose, because a
|
|
252
|
+
* merge table cannot cover every number.
|
|
253
|
+
*
|
|
254
|
+
* Corrected in isolation, which is why it can be trusted: changing only
|
|
255
|
+
* this takes that sample from -30.6% to -5.0% and moves nothing else more
|
|
256
|
+
* than four points. See `test/fixtures/token-ground-truth.json`.
|
|
257
|
+
*/
|
|
258
|
+
total += Math.ceil(n / 1.5);
|
|
101
259
|
continue;
|
|
102
260
|
}
|
|
103
261
|
|
|
@@ -133,7 +291,8 @@ export function estimateTokens(text: string): number {
|
|
|
133
291
|
}
|
|
134
292
|
}
|
|
135
293
|
|
|
136
|
-
|
|
294
|
+
// Rounded once, over the whole document, rather than per run — see the CJK branch.
|
|
295
|
+
return total + Math.ceil(cjkTokens);
|
|
137
296
|
}
|
|
138
297
|
|
|
139
298
|
/** Asynchronous token counter, for remote sources. */
|
package/src/types.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type AdvisorySeverity = 'info' | 'opportunity' | 'warning';
|
|
|
44
44
|
/** Every advisory the core can emit. */
|
|
45
45
|
export type AdvisoryId =
|
|
46
46
|
| 'context-overflow'
|
|
47
|
+
| 'context-near-limit'
|
|
47
48
|
| 'prompt-caching'
|
|
48
49
|
| 'prompt-caching-not-worth-it'
|
|
49
50
|
| 'below-cache-minimum'
|