@trazum/core 1.9.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +12 -5
  2. package/dist/advisories.d.ts.map +1 -1
  3. package/dist/advisories.js +94 -4
  4. package/dist/advisories.js.map +1 -1
  5. package/dist/against.d.ts +32 -0
  6. package/dist/against.d.ts.map +1 -0
  7. package/dist/against.js +34 -0
  8. package/dist/against.js.map +1 -0
  9. package/dist/config-schema.d.ts +42 -1
  10. package/dist/config-schema.d.ts.map +1 -1
  11. package/dist/config-schema.js +61 -0
  12. package/dist/config-schema.js.map +1 -1
  13. package/dist/conversation.d.ts +121 -0
  14. package/dist/conversation.d.ts.map +1 -0
  15. package/dist/conversation.js +157 -0
  16. package/dist/conversation.js.map +1 -0
  17. package/dist/csv.d.ts +61 -0
  18. package/dist/csv.d.ts.map +1 -0
  19. package/dist/csv.js +149 -0
  20. package/dist/csv.js.map +1 -0
  21. package/dist/evaluate.d.ts +24 -0
  22. package/dist/evaluate.d.ts.map +1 -1
  23. package/dist/evaluate.js +5 -2
  24. package/dist/evaluate.js.map +1 -1
  25. package/dist/i18n/en.d.ts.map +1 -1
  26. package/dist/i18n/en.js +18 -7
  27. package/dist/i18n/en.js.map +1 -1
  28. package/dist/i18n/es.d.ts.map +1 -1
  29. package/dist/i18n/es.js +18 -7
  30. package/dist/i18n/es.js.map +1 -1
  31. package/dist/i18n/types.d.ts +28 -1
  32. package/dist/i18n/types.d.ts.map +1 -1
  33. package/dist/index.d.ts +25 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +28 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/input-shape.d.ts +104 -0
  38. package/dist/input-shape.d.ts.map +1 -0
  39. package/dist/input-shape.js +132 -0
  40. package/dist/input-shape.js.map +1 -0
  41. package/dist/levers.d.ts +151 -0
  42. package/dist/levers.d.ts.map +1 -0
  43. package/dist/levers.js +160 -0
  44. package/dist/levers.js.map +1 -0
  45. package/dist/node.d.ts +1 -1
  46. package/dist/node.d.ts.map +1 -1
  47. package/dist/output-shape.d.ts +96 -0
  48. package/dist/output-shape.d.ts.map +1 -0
  49. package/dist/output-shape.js +145 -0
  50. package/dist/output-shape.js.map +1 -0
  51. package/dist/pricing-overlay.d.ts +1 -1
  52. package/dist/pricing-overlay.d.ts.map +1 -1
  53. package/dist/pricing-overlay.js +46 -0
  54. package/dist/pricing-overlay.js.map +1 -1
  55. package/dist/repeats.d.ts +75 -0
  56. package/dist/repeats.d.ts.map +1 -0
  57. package/dist/repeats.js +82 -0
  58. package/dist/repeats.js.map +1 -0
  59. package/dist/reprice.d.ts +143 -0
  60. package/dist/reprice.d.ts.map +1 -0
  61. package/dist/reprice.js +82 -0
  62. package/dist/reprice.js.map +1 -0
  63. package/dist/session-cost.d.ts +70 -0
  64. package/dist/session-cost.d.ts.map +1 -0
  65. package/dist/session-cost.js +90 -0
  66. package/dist/session-cost.js.map +1 -0
  67. package/dist/session-ledger.d.ts +77 -0
  68. package/dist/session-ledger.d.ts.map +1 -0
  69. package/dist/session-ledger.js +99 -0
  70. package/dist/session-ledger.js.map +1 -0
  71. package/dist/tokenizer.d.ts +33 -26
  72. package/dist/tokenizer.d.ts.map +1 -1
  73. package/dist/tokenizer.js +95 -34
  74. package/dist/tokenizer.js.map +1 -1
  75. package/dist/ttl-fit.d.ts +103 -0
  76. package/dist/ttl-fit.d.ts.map +1 -0
  77. package/dist/ttl-fit.js +184 -0
  78. package/dist/ttl-fit.js.map +1 -0
  79. package/dist/types.d.ts +1 -1
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/usage.d.ts +637 -0
  82. package/dist/usage.d.ts.map +1 -0
  83. package/dist/usage.js +634 -0
  84. package/dist/usage.js.map +1 -0
  85. package/package.json +1 -1
  86. package/src/advisories.ts +96 -4
  87. package/src/against.ts +48 -0
  88. package/src/config-schema.ts +106 -0
  89. package/src/conversation.ts +305 -0
  90. package/src/csv.ts +184 -0
  91. package/src/evaluate.ts +33 -3
  92. package/src/i18n/en.ts +19 -6
  93. package/src/i18n/es.ts +19 -6
  94. package/src/i18n/types.ts +29 -1
  95. package/src/index.ts +65 -1
  96. package/src/input-shape.ts +259 -0
  97. package/src/levers.ts +331 -0
  98. package/src/node.ts +1 -1
  99. package/src/output-shape.ts +254 -0
  100. package/src/pricing-overlay.ts +52 -1
  101. package/src/repeats.ts +166 -0
  102. package/src/reprice.ts +227 -0
  103. package/src/session-cost.ts +170 -0
  104. package/src/session-ledger.ts +189 -0
  105. package/src/tokenizer.ts +93 -30
  106. package/src/ttl-fit.ts +251 -0
  107. package/src/types.ts +1 -0
  108. package/src/usage.ts +1267 -0
@@ -0,0 +1,90 @@
1
+ import { effectivePricing, multipliersFor } from './pricing.js';
2
+ import { UNLABELLED } from './usage.js';
3
+ /** Every billed dollar of one call, at its own model's rates. */
4
+ function costOf(record, catalogue, on) {
5
+ const model = catalogue.byId.get(record.model);
6
+ if (!model)
7
+ return null;
8
+ const { inputPerMTok, outputPerMTok } = effectivePricing(model, on);
9
+ const rates = multipliersFor(model);
10
+ const per = (tokens, rate) => (tokens / 1_000_000) * rate;
11
+ return (per(record.inputTokens, inputPerMTok) +
12
+ per(record.cacheReadTokens, inputPerMTok * rates.cacheRead) +
13
+ per(record.cacheWrite5mTokens, inputPerMTok * rates.cacheWrite5m) +
14
+ per(record.cacheWrite1hTokens, inputPerMTok * rates.cacheWrite1h) +
15
+ per(record.outputTokens, outputPerMTok));
16
+ }
17
+ const median = (sorted) => {
18
+ const mid = Math.floor(sorted.length / 2);
19
+ return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
20
+ };
21
+ /**
22
+ * The 95th percentile by **nearest rank**: the smallest value at or above which
23
+ * 95% of the conversations sit. Interpolating between two conversations would
24
+ * report a cost no conversation had, and this figure exists to be compared
25
+ * against a real quota.
26
+ */
27
+ const p95 = (sorted) => sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1)];
28
+ export function createSessionCostTracker(options) {
29
+ const { catalogue, on = new Date(), minSessions = 5 } = options;
30
+ const slices = new Map();
31
+ const add = (record) => {
32
+ if (record.session === null)
33
+ return;
34
+ const cost = costOf(record, catalogue, on);
35
+ // An unpriced model contributes no dollars anywhere else either.
36
+ if (cost === null)
37
+ return;
38
+ const sliceKey = `${record.label ?? UNLABELLED}\n${record.model}`;
39
+ let sessions = slices.get(sliceKey);
40
+ if (!sessions) {
41
+ sessions = new Map();
42
+ slices.set(sliceKey, sessions);
43
+ }
44
+ const existing = sessions.get(record.session);
45
+ if (existing) {
46
+ existing.usd += cost;
47
+ existing.turns += 1;
48
+ }
49
+ else {
50
+ sessions.set(record.session, { usd: cost, turns: 1 });
51
+ }
52
+ };
53
+ const finish = () => {
54
+ const out = [];
55
+ for (const [sliceKey, sessions] of slices) {
56
+ if (sessions.size < minSessions)
57
+ continue;
58
+ const split = sliceKey.indexOf('\n');
59
+ const modelId = sliceKey.slice(split + 1);
60
+ const model = catalogue.byId.get(modelId);
61
+ if (!model)
62
+ continue;
63
+ const costs = [...sessions.values()].map((s) => s.usd).sort((a, b) => a - b);
64
+ const turns = [...sessions.values()].map((s) => s.turns).sort((a, b) => a - b);
65
+ out.push({
66
+ label: sliceKey.slice(0, split),
67
+ model: modelId,
68
+ modelName: model.displayName,
69
+ sessions: sessions.size,
70
+ calls: turns.reduce((sum, t) => sum + t, 0),
71
+ totalUsd: costs.reduce((sum, c) => sum + c, 0),
72
+ medianUsd: median(costs),
73
+ p95Usd: p95(costs),
74
+ maxUsd: costs[costs.length - 1],
75
+ medianTurns: median(turns),
76
+ });
77
+ }
78
+ // The most money first — the order somebody would act in.
79
+ return out.sort((a, b) => b.totalUsd - a.totalUsd || a.label.localeCompare(b.label));
80
+ };
81
+ return { add, finish };
82
+ }
83
+ /** The same measurement over a list, for a caller holding one already. */
84
+ export function sessionCostShapes(records, options) {
85
+ const tracker = createSessionCostTracker(options);
86
+ for (const record of records)
87
+ tracker.add(record);
88
+ return tracker.finish();
89
+ }
90
+ //# sourceMappingURL=session-cost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-cost.js","sourceRoot":"","sources":["../src/session-cost.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import type { PricingCatalogue } from './pricing.js';
2
+ import type { UsageRecord } from './usage.js';
3
+ /**
4
+ * Cache writes made by conversations that never came back.
5
+ *
6
+ * ## The waste the aggregate hides
7
+ *
8
+ * A cache write is a bet: pay 1.25x input now (2x at the 1-hour TTL) so the
9
+ * *next* call reads the prefix at 0.1x. A conversation that ends after its
10
+ * first turn never places that next call — its write bought reuse that its own
11
+ * conversation never made. On a workload with many short sessions this is a
12
+ * steady leak, and it hides inside healthy-looking totals: the long sessions'
13
+ * reads pay for the cache overall, so `cacheEconomics` reports `paid-off`
14
+ * while every one-turn drive-by pays the premium for nothing.
15
+ *
16
+ * ## The caveat that keeps the figure honest
17
+ *
18
+ * The provider's cache is keyed by prefix content, not by conversation. A
19
+ * one-turn session's write **can** be read back by a different session that
20
+ * sends the same prefix within the TTL — a shared system prompt does exactly
21
+ * that — and a usage log cannot see whose write a read hit. So the figure
22
+ * reported here is a **ceiling, named as one**: these writes paid off only if
23
+ * another conversation shared the prefix in time, and the log cannot say
24
+ * whether one did.
25
+ *
26
+ * There is one case where the ceiling collapses into a fact, and the caller
27
+ * can detect it from the slice it already has: when the slice recorded **zero
28
+ * cache reads**, nothing read those writes — within the session, across
29
+ * sessions, at all. The row deliberately does not decide this itself; the
30
+ * breakdown holding the slice's reads belongs to the caller, and deriving it
31
+ * twice is how two figures drift.
32
+ *
33
+ * Session keys group turns and never leave this module, like everywhere else
34
+ * the field is touched.
35
+ */
36
+ export interface SingleTurnCacheWrites {
37
+ label: string;
38
+ model: string;
39
+ modelName: string;
40
+ /** Conversations seen in this slice — with a session key, priced model. */
41
+ sessions: number;
42
+ /** Median turns per conversation, for scale: 1-turn sessions in a sea of 40-turn ones read differently than in a sea of 2s. */
43
+ medianTurns: number;
44
+ /** Conversations that ended after exactly one recorded turn. */
45
+ singleTurnSessions: number;
46
+ /** Cache-write tokens those one-turn conversations paid for. */
47
+ singleTurnWriteTokens: number;
48
+ /**
49
+ * What those writes cost, at the same rates the bill used — the 5-minute
50
+ * rate for writes whose TTL the log did not state, so like the bill it is
51
+ * a floor when `assumedTtlTokens` is non-zero. A **ceiling on the waste**
52
+ * (another conversation may have read the prefix; the log cannot see it)
53
+ * built on a **floor of a price** — both directions named, neither guessed.
54
+ */
55
+ singleTurnWriteUsd: number;
56
+ /** The part of `singleTurnWriteTokens` whose TTL the log did not record. */
57
+ assumedTtlTokens: number;
58
+ }
59
+ export interface SessionLedgerOptions {
60
+ catalogue: PricingCatalogue;
61
+ /** Date the prices are read at, so a promotional rate resolves the same way. */
62
+ on?: Date;
63
+ }
64
+ export interface SessionLedgerTracker {
65
+ add(record: UsageRecord): void;
66
+ finish(): SingleTurnCacheWrites[];
67
+ }
68
+ /**
69
+ * An accumulator, like the TTL-fit tracker and for the same reason: one pass
70
+ * over a log measured in megabytes, holding one small tally per conversation.
71
+ * No timestamp needed — "came back" is a fact about turn count, not the clock,
72
+ * so this measures logs the TTL-fit cannot.
73
+ */
74
+ export declare function createSessionLedgerTracker(options: SessionLedgerOptions): SessionLedgerTracker;
75
+ /** The same measurement over a list, for a caller holding one already. */
76
+ export declare function singleTurnCacheWrites(records: readonly UsageRecord[], options: SessionLedgerOptions): SingleTurnCacheWrites[];
77
+ //# sourceMappingURL=session-ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-ledger.d.ts","sourceRoot":"","sources":["../src/session-ledger.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,+HAA+H;IAC/H,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,qBAAqB,EAAE,MAAM,CAAC;IAC9B;;;;;;OAMG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,gFAAgF;IAChF,EAAE,CAAC,EAAE,IAAI,CAAC;CACX;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,MAAM,IAAI,qBAAqB,EAAE,CAAC;CACnC;AAcD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAsF9F;AAED,0EAA0E;AAC1E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,OAAO,EAAE,oBAAoB,GAC5B,qBAAqB,EAAE,CAIzB"}
@@ -0,0 +1,99 @@
1
+ import { effectivePricing, multipliersFor } from './pricing.js';
2
+ import { UNLABELLED } from './usage.js';
3
+ const median = (sorted) => {
4
+ const mid = Math.floor(sorted.length / 2);
5
+ return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
6
+ };
7
+ /**
8
+ * An accumulator, like the TTL-fit tracker and for the same reason: one pass
9
+ * over a log measured in megabytes, holding one small tally per conversation.
10
+ * No timestamp needed — "came back" is a fact about turn count, not the clock,
11
+ * so this measures logs the TTL-fit cannot.
12
+ */
13
+ export function createSessionLedgerTracker(options) {
14
+ const { catalogue, on = new Date() } = options;
15
+ const slices = new Map();
16
+ const add = (record) => {
17
+ // An unpriced model has no rate to price the waste at, and contributes no
18
+ // dollars anywhere else either.
19
+ if (record.session === null || !catalogue.byId.has(record.model))
20
+ return;
21
+ const key = `${record.label ?? UNLABELLED}\n${record.model}`;
22
+ let sessions = slices.get(key);
23
+ if (!sessions) {
24
+ sessions = new Map();
25
+ slices.set(key, sessions);
26
+ }
27
+ let tally = sessions.get(record.session);
28
+ if (!tally) {
29
+ tally = { turns: 0, write5mTokens: 0, write1hTokens: 0, assumedTokens: 0 };
30
+ sessions.set(record.session, tally);
31
+ }
32
+ tally.turns += 1;
33
+ if (record.writeTtlKnown) {
34
+ tally.write5mTokens += record.cacheWrite5mTokens;
35
+ tally.write1hTokens += record.cacheWrite1hTokens;
36
+ }
37
+ else {
38
+ // The flat count sits in the 5m bucket by pricing convention; kept
39
+ // apart here so the row can say how much of its price is a floor.
40
+ tally.assumedTokens += record.cacheWrite5mTokens;
41
+ }
42
+ };
43
+ const finish = () => {
44
+ const out = [];
45
+ for (const [key, sessions] of slices) {
46
+ let singleTurnSessions = 0;
47
+ let write5m = 0;
48
+ let write1h = 0;
49
+ let assumed = 0;
50
+ const turnCounts = [];
51
+ for (const tally of sessions.values()) {
52
+ turnCounts.push(tally.turns);
53
+ if (tally.turns !== 1)
54
+ continue;
55
+ singleTurnSessions += 1;
56
+ write5m += tally.write5mTokens;
57
+ write1h += tally.write1hTokens;
58
+ assumed += tally.assumedTokens;
59
+ }
60
+ const singleTurnWriteTokens = write5m + write1h + assumed;
61
+ // One-turn conversations that wrote nothing wasted nothing; a row about
62
+ // them would be a finding about the absence of a finding.
63
+ if (singleTurnWriteTokens === 0)
64
+ continue;
65
+ const split = key.indexOf('\n');
66
+ const modelId = key.slice(split + 1);
67
+ const model = catalogue.byId.get(modelId);
68
+ const { inputPerMTok } = effectivePricing(model, on);
69
+ const rates = multipliersFor(model);
70
+ const per = (tokens, rate) => (tokens / 1_000_000) * inputPerMTok * rate;
71
+ // The bill's own convention: unstated TTLs at the cheaper rate, so this
72
+ // is the same floor the totals already stand on — never a new guess.
73
+ const singleTurnWriteUsd = per(write5m + assumed, rates.cacheWrite5m) + per(write1h, rates.cacheWrite1h);
74
+ turnCounts.sort((a, b) => a - b);
75
+ out.push({
76
+ label: key.slice(0, split),
77
+ model: modelId,
78
+ modelName: model.displayName,
79
+ sessions: sessions.size,
80
+ medianTurns: median(turnCounts),
81
+ singleTurnSessions,
82
+ singleTurnWriteTokens,
83
+ singleTurnWriteUsd,
84
+ assumedTtlTokens: assumed,
85
+ });
86
+ }
87
+ // The most money first — the order somebody would act in.
88
+ return out.sort((a, b) => b.singleTurnWriteUsd - a.singleTurnWriteUsd || a.label.localeCompare(b.label));
89
+ };
90
+ return { add, finish };
91
+ }
92
+ /** The same measurement over a list, for a caller holding one already. */
93
+ export function singleTurnCacheWrites(records, options) {
94
+ const tracker = createSessionLedgerTracker(options);
95
+ for (const record of records)
96
+ tracker.add(record);
97
+ return tracker.finish();
98
+ }
99
+ //# sourceMappingURL=session-ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-ledger.js","sourceRoot":"","sources":["../src/session-ledger.ts"],"names":[],"mappings":"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"}
@@ -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 ±15%, which is plenty for comparing
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
@@ -23,36 +23,43 @@
23
23
  /**
24
24
  * The error band this estimator is published under, as a percentage.
25
25
  *
26
- * **Measured, not chosen.** It was `15` for eight releases — a design target
27
- * nobody had checked — and the first run of `scripts/measure-token-band.mjs`
28
- * against the official counting endpoint found two of eight samples outside it,
29
- * both underestimating. Underestimating tokens means under-reporting cost, which
30
- * is the flattering direction and the worst one for this tool.
26
+ * **Measured, not chosen, and it has moved three times.** It was `15` for eight
27
+ * releases as a design target nobody had checked; the first measurement found two
28
+ * of eight samples outside it and it went to `25`; fixing the digit divisor and
29
+ * calibrating per language brought it back to `15`. This is the fourth value and
30
+ * the first one that is comfortably above what the corpus actually shows.
31
31
  *
32
- * `15` is the measured worst case rounded up, by the same rule that briefly made
33
- * it 25: the corpus now tops out at 11.2%, on Japanese. It landing back on the
34
- * number that was a guess for eight releases is a coincidence and not a
35
- * restoration — that 15 bounded nothing, and this one bounds eleven measured
36
- * samples across four languages and six text types.
32
+ * ```
33
+ * worst measured error 6.4% (code-heavy, which nothing is fitted to)
34
+ * published band 10%
35
+ * ```
37
36
  *
38
- * **Read one caveat before trusting it.** Four of those samples are the Latin
39
- * languages whose divisors in `DIVISOR_BY_LANGUAGE` were calibrated on one or two
40
- * samples each, so their residuals are in-sample and optimistic by construction.
41
- * The band is set by the seven samples nothing was fitted to — worst 11.2% — and
42
- * the honest test of it is the next held-out sample in Spanish, French or German.
43
- * The corpus grows one sample at a time now, so that test is cheap to run.
37
+ * **The margin is deliberate and it is not slack.** 6.4 rounded up is 7, and
38
+ * publishing 7 would be a tighter claim than twenty-one samples across six text
39
+ * types can support: the corpus has no Korean, no Arabic, no Cyrillic prose, no
40
+ * mixed-script document, and a seventh text type could easily land at eight. A
41
+ * band that becomes false the first time somebody measures something new is the
42
+ * exact fault this whole exercise was fixing. Overstating the uncertainty is the
43
+ * safe direction for a tool that reports money.
44
44
  *
45
- * **What got it there was not accents.** A Spanish sample with zero accented
46
- * characters measured -22.9% against -22.1% for accented Spanish, which killed
47
- * diacritics as a signal. Languages differ in how many tokens their words cost
48
- * English 3.44 characters per token, German 2.02 so the estimator detects the
49
- * language and divides accordingly. See `language.ts`.
45
+ * What earned the drop from 15 was **splitting kana from han**. Every CJK
46
+ * character was charged one token, which put Japanese at +11.2% the worst error
47
+ * anywhere in the corpus while Chinese sat at −3.2% under the same rule. Kana
48
+ * measure 0.75 tokens per character and han 1.05, and that pair takes the two
49
+ * samples to −1.5% and +1.3%. See `KANA_TOKENS_PER_CHAR`.
50
50
  *
51
- * Exported so every report, README and tool description reads the same number.
52
- * It was a literal in twenty-four files before this, with the only machine-
53
- * readable copy in a test.
51
+ * **Which samples the band rests on matters more than the number.** Eight of the
52
+ * twenty-one had a constant fitted to them seven Latin divisors and the digit
53
+ * divisor so their residuals are optimistic by construction. The two worst
54
+ * errors in the corpus, `code-heavy` at 6.4% and `punctuation-heavy` at 5.7%, are
55
+ * fitted to nothing at all, and they are what sets this figure.
56
+ *
57
+ * Exported so every report, README and tool description reads the same number. It
58
+ * was a literal in twenty-four files before this, with the only machine-readable
59
+ * copy in a test, and `token-band.test.js` now fails any file that states a
60
+ * different one.
54
61
  */
55
- export declare const ESTIMATE_ERROR_BAND_PCT = 15;
62
+ export declare const ESTIMATE_ERROR_BAND_PCT = 10;
56
63
  /**
57
64
  * Estimates how many tokens `text` occupies.
58
65
  *
@@ -1 +1 @@
1
- {"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAwD1C;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA0GnD;AAED,sDAAsD;AACtD,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAElE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,uBAAuB,GAAG,iBAAiB,CA6BxF"}
1
+ {"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAmG1C;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuHnD;AAED,sDAAsD;AACtD,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAElE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,uBAAuB,GAAG,iBAAiB,CA6BxF"}
package/dist/tokenizer.js 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 ±15%, which is plenty for comparing
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
@@ -25,37 +25,65 @@ import { SAFE_FETCH_INIT, checkedEndpoint } from './net.js';
25
25
  /**
26
26
  * The error band this estimator is published under, as a percentage.
27
27
  *
28
- * **Measured, not chosen.** It was `15` for eight releases — a design target
29
- * nobody had checked — and the first run of `scripts/measure-token-band.mjs`
30
- * against the official counting endpoint found two of eight samples outside it,
31
- * both underestimating. Underestimating tokens means under-reporting cost, which
32
- * is the flattering direction and the worst one for this tool.
33
- *
34
- * `15` is the measured worst case rounded up, by the same rule that briefly made
35
- * it 25: the corpus now tops out at 11.2%, on Japanese. It landing back on the
36
- * number that was a guess for eight releases is a coincidence and not a
37
- * restoration — that 15 bounded nothing, and this one bounds eleven measured
38
- * samples across four languages and six text types.
39
- *
40
- * **Read one caveat before trusting it.** Four of those samples are the Latin
41
- * languages whose divisors in `DIVISOR_BY_LANGUAGE` were calibrated on one or two
42
- * samples each, so their residuals are in-sample and optimistic by construction.
43
- * The band is set by the seven samples nothing was fitted to worst 11.2% — and
44
- * the honest test of it is the next held-out sample in Spanish, French or German.
45
- * The corpus grows one sample at a time now, so that test is cheap to run.
46
- *
47
- * **What got it there was not accents.** A Spanish sample with zero accented
48
- * characters measured -22.9% against -22.1% for accented Spanish, which killed
49
- * diacritics as a signal. Languages differ in how many tokens their words cost
50
- * English 3.44 characters per token, German 2.02 so the estimator detects the
51
- * language and divides accordingly. See `language.ts`.
52
- *
53
- * Exported so every report, README and tool description reads the same number.
54
- * It was a literal in twenty-four files before this, with the only machine-
55
- * readable copy in a test.
28
+ * **Measured, not chosen, and it has moved three times.** It was `15` for eight
29
+ * releases as a design target nobody had checked; the first measurement found two
30
+ * of eight samples outside it and it went to `25`; fixing the digit divisor and
31
+ * calibrating per language brought it back to `15`. This is the fourth value and
32
+ * the first one that is comfortably above what the corpus actually shows.
33
+ *
34
+ * ```
35
+ * worst measured error 6.4% (code-heavy, which nothing is fitted to)
36
+ * published band 10%
37
+ * ```
38
+ *
39
+ * **The margin is deliberate and it is not slack.** 6.4 rounded up is 7, and
40
+ * publishing 7 would be a tighter claim than twenty-one samples across six text
41
+ * types can support: the corpus has no Korean, no Arabic, no Cyrillic prose, no
42
+ * mixed-script document, and a seventh text type could easily land at eight. A
43
+ * band that becomes false the first time somebody measures something new is the
44
+ * exact fault this whole exercise was fixing. Overstating the uncertainty is the
45
+ * safe direction for a tool that reports money.
46
+ *
47
+ * What earned the drop from 15 was **splitting kana from han**. Every CJK
48
+ * character was charged one token, which put Japanese at +11.2% the worst error
49
+ * anywhere in the corpus while Chinese sat at −3.2% under the same rule. Kana
50
+ * measure 0.75 tokens per character and han 1.05, and that pair takes the two
51
+ * samples to −1.5% and +1.3%. See `KANA_TOKENS_PER_CHAR`.
52
+ *
53
+ * **Which samples the band rests on matters more than the number.** Eight of the
54
+ * twenty-one had a constant fitted to them seven Latin divisors and the digit
55
+ * divisor so their residuals are optimistic by construction. The two worst
56
+ * errors in the corpus, `code-heavy` at 6.4% and `punctuation-heavy` at 5.7%, are
57
+ * fitted to nothing at all, and they are what sets this figure.
58
+ *
59
+ * Exported so every report, README and tool description reads the same number. It
60
+ * was a literal in twenty-four files before this, with the only machine-readable
61
+ * copy in a test, and `token-band.test.js` now fails any file that states a
62
+ * different one.
56
63
  */
57
- export const ESTIMATE_ERROR_BAND_PCT = 15;
64
+ export const ESTIMATE_ERROR_BAND_PCT = 10;
58
65
  const CJK = /[぀-ヿ㐀-䶿一-鿿가-힯]/;
66
+ /**
67
+ * Kana, separated from the rest of CJK because they do not cost the same.
68
+ *
69
+ * **This was the largest error left in the corpus.** Every CJK character was
70
+ * charged one token, and measured against the counting endpoint that put Japanese
71
+ * at **+11.2%** — the worst figure anywhere in twenty-one samples — while Chinese
72
+ * came out at −3.2% under the identical rule. One constant cannot be right for
73
+ * both, and the reason is visible in the samples: the Japanese one is 58% kana and
74
+ * the Chinese one is 0%.
75
+ *
76
+ * Kana are a small syllabary that appears in every sentence, so the merge table
77
+ * covers runs of them and several characters share a token. Han are tens of
78
+ * thousands of rare characters; a merge table cannot cover them and they cost
79
+ * about one each, sometimes more.
80
+ *
81
+ * The signal needs no detector. A character is kana or it is not, and the two
82
+ * samples separate perfectly — 58.3% against 0.00% — so this is a property of the
83
+ * character rather than a guess about the document. That is the difference between
84
+ * this and `language.ts`, which has to decide and is allowed to refuse.
85
+ */
86
+ const KANA = /[぀-ヿ]/;
59
87
  const LETTER = /[A-Za-zÀ-ɏͰ-ϿЀ-ӿ]/;
60
88
  const DIGIT = /[0-9]/;
61
89
  /**
@@ -90,6 +118,26 @@ const DIVISOR_BY_LANGUAGE = {
90
118
  pt: 3.05,
91
119
  nl: 2.65,
92
120
  };
121
+ /**
122
+ * Tokens per character for the two halves of CJK, measured.
123
+ *
124
+ * `0.75` and `1.05` come from a search over the two CJK samples in
125
+ * `test/fixtures/token-ground-truth.json`, and they take that pair from
126
+ * +11.2% / −3.2% to −1.5% / +1.3%.
127
+ *
128
+ * **Two samples fitted two constants, so those residuals are in-sample and
129
+ * optimistic by construction** — the same caveat the Latin divisors carry, stated
130
+ * for the same reason. What makes them worth having anyway is the size of the
131
+ * error they replace and the fact that they move in opposite directions: a single
132
+ * constant could not have been within four points of both, whatever it was set to.
133
+ * The honest test is a third CJK sample, and the corpus grows one at a time.
134
+ *
135
+ * Hangul keeps the old cost of 1, because nothing here measures Korean. Guessing
136
+ * it from Japanese would be inventing a figure — the two scripts have nothing in
137
+ * common that would make one predict the other.
138
+ */
139
+ const KANA_TOKENS_PER_CHAR = 0.75;
140
+ const HAN_TOKENS_PER_CHAR = 1.05;
93
141
  /**
94
142
  * What a prompt gets when the language could not be told.
95
143
  *
@@ -126,6 +174,11 @@ export function estimateTokens(text) {
126
174
  const language = detectTextLanguage(text);
127
175
  const divisor = (language === null ? undefined : DIVISOR_BY_LANGUAGE[language]) ?? DEFAULT_DIVISOR;
128
176
  let total = 0;
177
+ /**
178
+ * CJK is summed separately because it is the one class counted in fractions of
179
+ * a token. Everything else is whole tokens by the character class it belongs to.
180
+ */
181
+ let cjkTokens = 0;
129
182
  let i = 0;
130
183
  const chars = Array.from(text);
131
184
  while (i < chars.length) {
@@ -144,12 +197,19 @@ export function estimateTokens(text) {
144
197
  continue;
145
198
  }
146
199
  if (CJK.test(ch)) {
147
- let n = 0;
200
+ /**
201
+ * Accumulated as a fraction and rounded once, at the end.
202
+ *
203
+ * Rounding up per run was the first attempt and it was wrong by five
204
+ * points. Ordinary Japanese alternates kana and han inside every sentence,
205
+ * so the runs are short and there are many of them — and a `Math.ceil` per
206
+ * run charges most of a token for each boundary. That is an artefact of
207
+ * where the loop happens to break, not of what the text costs.
208
+ */
148
209
  while (i < chars.length && CJK.test(chars[i])) {
149
- n++;
210
+ cjkTokens += KANA.test(chars[i]) ? KANA_TOKENS_PER_CHAR : HAN_TOKENS_PER_CHAR;
150
211
  i++;
151
212
  }
152
- total += n;
153
213
  continue;
154
214
  }
155
215
  if (LETTER.test(ch)) {
@@ -212,7 +272,8 @@ export function estimateTokens(text) {
212
272
  total += Math.ceil(n / 2);
213
273
  }
214
274
  }
215
- return total;
275
+ // Rounded once, over the whole document, rather than per run — see the CJK branch.
276
+ return total + Math.ceil(cjkTokens);
216
277
  }
217
278
  /**
218
279
  * Exact counter using the official `/v1/messages/count_tokens` endpoint.
@@ -1 +1 @@
1
- {"version":3,"file":"tokenizer.js","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tokenizer.js","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"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"}