@trazum/core 1.81.0 → 1.83.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 +34 -18
- package/dist/advisories.d.ts +33 -1
- package/dist/advisories.d.ts.map +1 -1
- package/dist/advisories.js +130 -20
- package/dist/advisories.js.map +1 -1
- package/dist/band.d.ts +131 -0
- package/dist/band.d.ts.map +1 -0
- package/dist/band.js +210 -0
- package/dist/band.js.map +1 -0
- package/dist/conform.d.ts +1 -1
- package/dist/conform.d.ts.map +1 -1
- package/dist/conform.js +84 -1
- package/dist/conform.js.map +1 -1
- package/dist/helicone.d.ts +92 -0
- package/dist/helicone.d.ts.map +1 -0
- package/dist/helicone.js +194 -0
- package/dist/helicone.js.map +1 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +16 -3
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +17 -3
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +60 -1
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/langsmith.d.ts +104 -0
- package/dist/langsmith.d.ts.map +1 -0
- package/dist/langsmith.js +270 -0
- package/dist/langsmith.js.map +1 -0
- package/dist/levers.d.ts +1 -1
- package/dist/levers.js +2 -2
- package/dist/levers.js.map +1 -1
- package/dist/litellm.d.ts +98 -0
- package/dist/litellm.d.ts.map +1 -0
- package/dist/litellm.js +207 -0
- package/dist/litellm.js.map +1 -0
- package/dist/measured-profile.d.ts +4 -0
- package/dist/measured-profile.d.ts.map +1 -1
- package/dist/measured-profile.js +22 -1
- package/dist/measured-profile.js.map +1 -1
- package/dist/pricing.d.ts +98 -3
- package/dist/pricing.d.ts.map +1 -1
- package/dist/pricing.js +393 -35
- package/dist/pricing.js.map +1 -1
- package/dist/receipt.d.ts +253 -0
- package/dist/receipt.d.ts.map +1 -0
- package/dist/receipt.js +111 -0
- package/dist/receipt.js.map +1 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +36 -0
- package/dist/schemas.js.map +1 -1
- package/dist/tokenizer.d.ts +45 -49
- package/dist/tokenizer.d.ts.map +1 -1
- package/dist/tokenizer.js +100 -51
- package/dist/tokenizer.js.map +1 -1
- package/dist/types.d.ts +108 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/verdict-bridge.d.ts +113 -0
- package/dist/verdict-bridge.d.ts.map +1 -0
- package/dist/verdict-bridge.js +127 -0
- package/dist/verdict-bridge.js.map +1 -0
- package/package.json +1 -1
- package/src/advisories.ts +166 -19
- package/src/band.ts +213 -0
- package/src/conform.ts +100 -1
- package/src/helicone.ts +227 -0
- package/src/i18n/en.ts +19 -3
- package/src/i18n/es.ts +20 -3
- package/src/i18n/types.ts +63 -1
- package/src/index.ts +31 -2
- package/src/langsmith.ts +315 -0
- package/src/levers.ts +3 -3
- package/src/litellm.ts +243 -0
- package/src/measured-profile.ts +26 -2
- package/src/pricing.ts +430 -30
- package/src/receipt.ts +351 -0
- package/src/schemas.ts +36 -0
- package/src/tokenizer.ts +101 -51
- package/src/types.ts +104 -0
- package/src/verdict-bridge.ts +200 -0
package/src/receipt.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { PROVIDER_REVIEWED, modelFrom } from './pricing.js';
|
|
2
|
+
import type { PricingCatalogue } from './pricing.js';
|
|
3
|
+
import { UNLABELLED } from './usage.js';
|
|
4
|
+
import type { UsageBreakdown, UsageProfileReport } from './usage.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The receipt: a bill's counts, with the provenance of every figure attached.
|
|
8
|
+
*
|
|
9
|
+
* ## What it is for
|
|
10
|
+
*
|
|
11
|
+
* A profile answers a question on the terminal where it ran. A receipt is the
|
|
12
|
+
* same figures shaped so they can still answer it somewhere else: aggregated by
|
|
13
|
+
* somebody's dashboard, filed against an invoice, or compared against last
|
|
14
|
+
* month's by a reader who was not there when the command ran.
|
|
15
|
+
*
|
|
16
|
+
* That is why every line carries the rate behind its money and the date that
|
|
17
|
+
* rate was last read. A dollar figure with no provenance cannot tell a
|
|
18
|
+
* repricing from a team whose spend moved, and the consumer that most needs
|
|
19
|
+
* that distinction is the one that cannot read a sentence explaining it.
|
|
20
|
+
*
|
|
21
|
+
* ## Why a line carries money per bucket and not only a rate
|
|
22
|
+
*
|
|
23
|
+
* The first version of this module published the catalogue's `inputPerMTok`
|
|
24
|
+
* and `outputPerMTok` beside a dollar figure computed by `profileUsage`. Those
|
|
25
|
+
* are not always the same rates. A model in a promotional window, or one whose
|
|
26
|
+
* long-context tier applied, is billed at `effectivePricing`'s figure, and a
|
|
27
|
+
* cached read is billed at a fraction of input that no published field named at
|
|
28
|
+
* all. **A consumer multiplying the stated rates by the stated tokens got a
|
|
29
|
+
* different number from the stated total, with nothing saying which was
|
|
30
|
+
* wrong.**
|
|
31
|
+
*
|
|
32
|
+
* That is a bad defect in a document whose entire purpose is that a figure can
|
|
33
|
+
* be recomputed by whoever receives it, and it is the exact failure the
|
|
34
|
+
* doctrine line at the foot of this comment is about.
|
|
35
|
+
*
|
|
36
|
+
* So a line now carries the **money as it was actually apportioned** -- input,
|
|
37
|
+
* cache reads, cache writes and output -- and `usd` is their sum. The rates stay
|
|
38
|
+
* as provenance, labelled as the catalogue's published figures rather than as
|
|
39
|
+
* the arithmetic. `receipt-arithmetic.test.js` fails if the four stop adding up,
|
|
40
|
+
* which is a property a reader can check on any receipt they are handed rather
|
|
41
|
+
* than a promise they have to take.
|
|
42
|
+
*
|
|
43
|
+
* ## What it carries, and what it has no field for
|
|
44
|
+
*
|
|
45
|
+
* Counts, model, provider, the money split by bucket, the published rates, the
|
|
46
|
+
* review date, the label the log carried, the period, and what could not be
|
|
47
|
+
* priced. There is **no field
|
|
48
|
+
* for prompt text, for an answer, for a file path, for a branch name or for a
|
|
49
|
+
* credential** — not redacted, not hashed, absent, because the input this takes
|
|
50
|
+
* has nowhere to hold them either.
|
|
51
|
+
*
|
|
52
|
+
* `receipt-redaction.test.js` holds that by planting all 4 in a log — in the
|
|
53
|
+
* session field among other places, which is the value this package has
|
|
54
|
+
* promised since it shipped never comes back out — and failing if any reaches
|
|
55
|
+
* the output. A property about what a document does not contain is exactly the
|
|
56
|
+
* kind this repository proves by breaking rather than by asserting.
|
|
57
|
+
*
|
|
58
|
+
* ## What it is not
|
|
59
|
+
*
|
|
60
|
+
* **Not a transport.** Nothing here opens a socket, reads an environment
|
|
61
|
+
* variable or takes an endpoint. `receiptFrom` returns a value; where it goes
|
|
62
|
+
* is the caller's decision. A module in this package that phoned home would
|
|
63
|
+
* break the roadmap's first rule, which is that no feature may make a network
|
|
64
|
+
* call a prerequisite for anything.
|
|
65
|
+
*
|
|
66
|
+
* **Not a new measurement.** Every figure is one `profileUsage` already
|
|
67
|
+
* produced. This adds provenance around them and takes nothing away.
|
|
68
|
+
*
|
|
69
|
+
* Doctrine: [A machine reader gets the provenance too](../../../docs/doctrine.md#a-machine-reader-gets-the-provenance-too)
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The catalogue's published rates for this model, and when a human last read
|
|
74
|
+
* them off the provider's own page.
|
|
75
|
+
*
|
|
76
|
+
* **Provenance, not the arithmetic.** These are list rates. The money that was
|
|
77
|
+
* actually apportioned is in `ReceiptLine.money`, and the two differ whenever a
|
|
78
|
+
* promotion or a long-context tier applied, or for any token billed at a
|
|
79
|
+
* fraction of input -- which is every cached read. A consumer wanting the rate
|
|
80
|
+
* that was really charged divides a bucket's money by its tokens; a consumer
|
|
81
|
+
* wanting to know whether a provider repriced compares these.
|
|
82
|
+
*/
|
|
83
|
+
export interface ReceiptPricing {
|
|
84
|
+
provider: string;
|
|
85
|
+
inputPerMTok: number;
|
|
86
|
+
outputPerMTok: number;
|
|
87
|
+
/**
|
|
88
|
+
* The date this provider's rates were last read off its own page, `null`
|
|
89
|
+
* when the catalogue carries no date for that provider.
|
|
90
|
+
*
|
|
91
|
+
* Per provider rather than per catalogue, because that is how the prices are
|
|
92
|
+
* actually reviewed: looking at one page and finding nothing changed is not
|
|
93
|
+
* the same event as not looking. A server comparing two receipts a month
|
|
94
|
+
* apart reads a repricing as a repricing instead of as a team missing a
|
|
95
|
+
* target.
|
|
96
|
+
*/
|
|
97
|
+
reviewedOn: string | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Where a line's money went. The four add to `ReceiptLine.usd`. */
|
|
101
|
+
export interface ReceiptMoney {
|
|
102
|
+
inputUsd: number;
|
|
103
|
+
cacheReadUsd: number;
|
|
104
|
+
cacheWriteUsd: number;
|
|
105
|
+
outputUsd: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One slice of a bill: a label's calls to one model. */
|
|
109
|
+
export interface ReceiptLine {
|
|
110
|
+
/**
|
|
111
|
+
* The attribution the log carried, or `null` for the unlabelled bucket.
|
|
112
|
+
*
|
|
113
|
+
* `null` rather than the sentinel, which is an internal grouping key and is
|
|
114
|
+
* never serialised. A consumer that saw the raw sentinel would have to know
|
|
115
|
+
* this package's internals to render it.
|
|
116
|
+
*/
|
|
117
|
+
label: string | null;
|
|
118
|
+
model: string;
|
|
119
|
+
calls: number;
|
|
120
|
+
inputTokens: number;
|
|
121
|
+
cacheReadTokens: number;
|
|
122
|
+
/** Every cache write, whatever its TTL. The sum of the two fields below. */
|
|
123
|
+
cacheWriteTokens: number;
|
|
124
|
+
/**
|
|
125
|
+
* The two write TTLs kept apart, because they are billed at different rates.
|
|
126
|
+
*
|
|
127
|
+
* They are here for the same reason `UsageBreakdown` keeps them: the ratio
|
|
128
|
+
* between them is not a constant across providers, so a total that has lost
|
|
129
|
+
* the split can be repriced only by guessing at it. A consumer repricing this
|
|
130
|
+
* traffic against another model's rates needs both.
|
|
131
|
+
*/
|
|
132
|
+
cacheWrite5mTokens: number;
|
|
133
|
+
cacheWrite1hTokens: number;
|
|
134
|
+
outputTokens: number;
|
|
135
|
+
/**
|
|
136
|
+
* What it cost, which is exactly the sum of `money` below.
|
|
137
|
+
*
|
|
138
|
+
* Every line here is priced; what is not is in `gaps`.
|
|
139
|
+
*/
|
|
140
|
+
usd: number;
|
|
141
|
+
/**
|
|
142
|
+
* Where the money went, as it was apportioned rather than as it might be
|
|
143
|
+
* recomputed.
|
|
144
|
+
*
|
|
145
|
+
* This is the field that makes a receipt checkable. `usd` alone can only be
|
|
146
|
+
* believed; these four can be added up, and each can be divided by its own
|
|
147
|
+
* token count to recover the rate that was really charged -- including the
|
|
148
|
+
* one for cached reads, which no published rate names.
|
|
149
|
+
*/
|
|
150
|
+
money: ReceiptMoney;
|
|
151
|
+
/** The catalogue's published rates, as provenance. */
|
|
152
|
+
pricing: ReceiptPricing;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Everything a receipt refuses to answer, typed rather than left implicit. */
|
|
156
|
+
export type ReceiptGap =
|
|
157
|
+
/**
|
|
158
|
+
* Models the catalogue does not price, with the size of what they used.
|
|
159
|
+
*
|
|
160
|
+
* The counts are here rather than in a line, and the first draft of this
|
|
161
|
+
* module got that wrong in a way its own guard caught. `byLabelAndModel`
|
|
162
|
+
* excludes unpriced calls on purpose — the profile keeps them apart so that
|
|
163
|
+
* a total is never tokens from one set of calls over dollars from another —
|
|
164
|
+
* so a receipt built from that list alone silently loses them.
|
|
165
|
+
*
|
|
166
|
+
* The obvious repair was worse than the bug: emit a line with `label: null`.
|
|
167
|
+
* That reads as *this call carried no label*, when in fact it carried one the
|
|
168
|
+
* profile could not attach to a priced slice. Conflating "unlabelled" with
|
|
169
|
+
* "unattributable" is the exact failure this project refuses elsewhere, so
|
|
170
|
+
* the gap carries the magnitude instead and nothing pretends to an
|
|
171
|
+
* attribution it does not have.
|
|
172
|
+
*/
|
|
173
|
+
| { kind: 'unpriced'; models: string[]; calls: number; inputTokens: number; outputTokens: number }
|
|
174
|
+
| { kind: 'unread-lines'; count: number }
|
|
175
|
+
| { kind: 'no-clock' }
|
|
176
|
+
/**
|
|
177
|
+
* Calls whose cache-write TTL the log did not state, so the cheaper rate was
|
|
178
|
+
* assumed.
|
|
179
|
+
*
|
|
180
|
+
* Reported because it changes what kind of figure this is. A total that
|
|
181
|
+
* includes an assumed TTL is a **floor** on those calls, not a measurement,
|
|
182
|
+
* and a document that says `counting: 'counted'` while quietly resting on an
|
|
183
|
+
* assumption is claiming a precision it does not have.
|
|
184
|
+
*/
|
|
185
|
+
| { kind: 'assumed-write-ttl'; calls: number };
|
|
186
|
+
|
|
187
|
+
export interface ReceiptDocument {
|
|
188
|
+
/** The one thing a consumer must branch on. Set here, by the builder. */
|
|
189
|
+
schemaVersion: 1;
|
|
190
|
+
/**
|
|
191
|
+
* When this receipt was made, or `null`.
|
|
192
|
+
*
|
|
193
|
+
* Optional on purpose, and the reason is the same one `plan.json` gives: a
|
|
194
|
+
* document without a stamp is undated, which a reader can act on. A document
|
|
195
|
+
* with an invented stamp is wrong, which they cannot.
|
|
196
|
+
*/
|
|
197
|
+
emittedAt: string | null;
|
|
198
|
+
/** The period the counts cover, from the log's own clock. */
|
|
199
|
+
span: { fromMs: number; toMs: number; calls: number } | null;
|
|
200
|
+
/**
|
|
201
|
+
* How the tokens were arrived at.
|
|
202
|
+
*
|
|
203
|
+
* `counted` for every receipt this function builds, because a usage log
|
|
204
|
+
* carries the provider's own counts. The field exists because a receipt built
|
|
205
|
+
* from an estimate would be a different kind of evidence, and a consumer must
|
|
206
|
+
* never have to guess which one it is holding.
|
|
207
|
+
*/
|
|
208
|
+
counting: 'counted';
|
|
209
|
+
lines: ReceiptLine[];
|
|
210
|
+
total: { calls: number; usd: number };
|
|
211
|
+
/**
|
|
212
|
+
* What this receipt cannot say, and why.
|
|
213
|
+
*
|
|
214
|
+
* Present and empty rather than absent when there is nothing to report:
|
|
215
|
+
* "nothing was refused" and "this document does not record refusals" are
|
|
216
|
+
* different statements, and a server aggregating a thousand receipts has to
|
|
217
|
+
* be able to tell them apart.
|
|
218
|
+
*/
|
|
219
|
+
gaps: ReceiptGap[];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The fields that leave this machine, listed once, in one place.
|
|
224
|
+
*
|
|
225
|
+
* The redaction guard reads this array rather than a copy of it, so a field
|
|
226
|
+
* added to `ReceiptLine` and not added here fails the guard. A whitelist
|
|
227
|
+
* maintained in two places is a whitelist with a hole in it.
|
|
228
|
+
*/
|
|
229
|
+
export const RECEIPT_LINE_FIELDS = Object.freeze([
|
|
230
|
+
'label',
|
|
231
|
+
'model',
|
|
232
|
+
'calls',
|
|
233
|
+
'inputTokens',
|
|
234
|
+
'cacheReadTokens',
|
|
235
|
+
'cacheWriteTokens',
|
|
236
|
+
'cacheWrite5mTokens',
|
|
237
|
+
'cacheWrite1hTokens',
|
|
238
|
+
'outputTokens',
|
|
239
|
+
'usd',
|
|
240
|
+
'money',
|
|
241
|
+
'pricing',
|
|
242
|
+
] as const);
|
|
243
|
+
|
|
244
|
+
const lineFrom = (
|
|
245
|
+
label: string,
|
|
246
|
+
model: string,
|
|
247
|
+
breakdown: UsageBreakdown,
|
|
248
|
+
catalogue: PricingCatalogue,
|
|
249
|
+
): ReceiptLine => {
|
|
250
|
+
/*
|
|
251
|
+
* Every slice reaching here is priced: `byLabelAndModel` holds only what the
|
|
252
|
+
* catalogue knows. What it does not know is reported by the `unpriced` gap,
|
|
253
|
+
* with its size, so the receipt's total is never mistaken for the whole bill.
|
|
254
|
+
*/
|
|
255
|
+
const priced = modelFrom(catalogue, model);
|
|
256
|
+
return {
|
|
257
|
+
label: label === UNLABELLED ? null : label,
|
|
258
|
+
model,
|
|
259
|
+
calls: breakdown.calls,
|
|
260
|
+
inputTokens: breakdown.inputTokens,
|
|
261
|
+
cacheReadTokens: breakdown.cacheReadTokens,
|
|
262
|
+
cacheWriteTokens: breakdown.cacheWriteTokens,
|
|
263
|
+
cacheWrite5mTokens: breakdown.cacheWrite5mTokens,
|
|
264
|
+
cacheWrite1hTokens: breakdown.cacheWrite1hTokens,
|
|
265
|
+
outputTokens: breakdown.outputTokens,
|
|
266
|
+
usd: breakdown.totalUsd,
|
|
267
|
+
/*
|
|
268
|
+
* Copied from the breakdown rather than recomputed from the rates below.
|
|
269
|
+
* Recomputing would reintroduce the defect this field exists to fix: the
|
|
270
|
+
* published rates are list rates, and a promotion, a long-context tier or
|
|
271
|
+
* any cached token is billed at something else.
|
|
272
|
+
*/
|
|
273
|
+
money: {
|
|
274
|
+
inputUsd: breakdown.inputUsd,
|
|
275
|
+
cacheReadUsd: breakdown.cacheReadUsd,
|
|
276
|
+
cacheWriteUsd: breakdown.cacheWriteUsd,
|
|
277
|
+
outputUsd: breakdown.outputUsd,
|
|
278
|
+
},
|
|
279
|
+
pricing: {
|
|
280
|
+
provider: priced.provider ?? 'unknown',
|
|
281
|
+
inputPerMTok: priced.inputPerMTok,
|
|
282
|
+
outputPerMTok: priced.outputPerMTok,
|
|
283
|
+
reviewedOn: PROVIDER_REVIEWED[priced.provider ?? ''] ?? null,
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export interface ReceiptOptions {
|
|
289
|
+
/** Stamp the document. Omit for an undated receipt, which is valid. */
|
|
290
|
+
emittedAt?: Date;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Build a receipt from a profile the caller already computed.
|
|
295
|
+
*
|
|
296
|
+
* Takes the report rather than the log text, and that is the guarantee rather
|
|
297
|
+
* than an ergonomic choice: `UsageProfileReport` has no field that can hold
|
|
298
|
+
* prompt content, so this function could not emit any even if it tried. The
|
|
299
|
+
* narrow input is what makes the promise checkable instead of merely stated.
|
|
300
|
+
*/
|
|
301
|
+
export function receiptFrom(
|
|
302
|
+
report: UsageProfileReport,
|
|
303
|
+
catalogue: PricingCatalogue,
|
|
304
|
+
options: ReceiptOptions = {},
|
|
305
|
+
): ReceiptDocument {
|
|
306
|
+
const lines = report.byLabelAndModel.map((slice) =>
|
|
307
|
+
lineFrom(slice.label, slice.model, slice.breakdown, catalogue),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const gaps: ReceiptGap[] = [];
|
|
311
|
+
if (report.unpricedModels.length > 0) {
|
|
312
|
+
gaps.push({
|
|
313
|
+
kind: 'unpriced',
|
|
314
|
+
models: [...report.unpricedModels],
|
|
315
|
+
calls: report.unpriced.calls,
|
|
316
|
+
inputTokens: report.unpriced.inputTokens,
|
|
317
|
+
outputTokens: report.unpriced.outputTokens,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (report.skippedLines.length > 0) {
|
|
321
|
+
gaps.push({ kind: 'unread-lines', count: report.skippedLines.length });
|
|
322
|
+
}
|
|
323
|
+
if (report.span === null) gaps.push({ kind: 'no-clock' });
|
|
324
|
+
|
|
325
|
+
/*
|
|
326
|
+
* Summed across the lines rather than read off a total, because the profile
|
|
327
|
+
* keeps this per slice and a receipt reports it once for the document.
|
|
328
|
+
*/
|
|
329
|
+
const assumed = report.byLabelAndModel.reduce(
|
|
330
|
+
(sum, slice) => sum + slice.breakdown.assumedWriteTtlCalls,
|
|
331
|
+
0,
|
|
332
|
+
);
|
|
333
|
+
if (assumed > 0) gaps.push({ kind: 'assumed-write-ttl', calls: assumed });
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
schemaVersion: 1,
|
|
337
|
+
emittedAt: options.emittedAt?.toISOString() ?? null,
|
|
338
|
+
span: report.span === null ? null : { ...report.span },
|
|
339
|
+
counting: 'counted',
|
|
340
|
+
lines,
|
|
341
|
+
/*
|
|
342
|
+
* `report.total` already excludes what could not be priced — the profile
|
|
343
|
+
* keeps those in `unpriced` precisely so a total is never tokens from one
|
|
344
|
+
* set of calls divided by dollars from another. The receipt inherits that
|
|
345
|
+
* separation rather than re-deriving it, and `gaps` carries the size of
|
|
346
|
+
* what is missing.
|
|
347
|
+
*/
|
|
348
|
+
total: { calls: report.total.calls, usd: report.total.totalUsd },
|
|
349
|
+
gaps,
|
|
350
|
+
};
|
|
351
|
+
}
|
package/src/schemas.ts
CHANGED
|
@@ -151,6 +151,42 @@ export const CONTRACT_SCHEMAS: Record<ContractName, Record<string, unknown>> = {
|
|
|
151
151
|
cannotSay: array,
|
|
152
152
|
unpricedRecords: number,
|
|
153
153
|
}),
|
|
154
|
+
'routing-measurement': doc('routing-measurement', {
|
|
155
|
+
slice: object,
|
|
156
|
+
evaluation: object,
|
|
157
|
+
}),
|
|
158
|
+
'example-pruning': doc('example-pruning', {
|
|
159
|
+
provider: string,
|
|
160
|
+
model: string,
|
|
161
|
+
selfAgreement: number,
|
|
162
|
+
recoverableTokens: number,
|
|
163
|
+
callsMade: number,
|
|
164
|
+
contributions: array,
|
|
165
|
+
}),
|
|
166
|
+
'store-inventory': doc('store-inventory', {
|
|
167
|
+
providers: array,
|
|
168
|
+
totalRecords: number,
|
|
169
|
+
span: objectOrNull,
|
|
170
|
+
possiblyDouble: number,
|
|
171
|
+
unknownVersion: number,
|
|
172
|
+
totalUsd: number,
|
|
173
|
+
unreadable: array,
|
|
174
|
+
}),
|
|
175
|
+
conformance: doc('conformance', {
|
|
176
|
+
contract: stringOrNull,
|
|
177
|
+
because: stringOrNull,
|
|
178
|
+
problems: array,
|
|
179
|
+
unavailable: array,
|
|
180
|
+
records: { type: ['number', 'null'] },
|
|
181
|
+
conforms: boolean,
|
|
182
|
+
}),
|
|
183
|
+
'watch-cycle': doc('watch-cycle', {
|
|
184
|
+
firedAtMs: number,
|
|
185
|
+
crossings: array,
|
|
186
|
+
suppressed: array,
|
|
187
|
+
abstentions: array,
|
|
188
|
+
gap: objectOrNull,
|
|
189
|
+
}),
|
|
154
190
|
};
|
|
155
191
|
|
|
156
192
|
/** The schema for one named contract — the object `trazum schema` prints. */
|
package/src/tokenizer.ts
CHANGED
|
@@ -2,20 +2,22 @@
|
|
|
2
2
|
* Dependency-free token estimator.
|
|
3
3
|
*
|
|
4
4
|
* This is NOT a real tokenizer: it is a heuristic calibrated per character
|
|
5
|
-
* class
|
|
6
|
-
*
|
|
7
|
-
* two versions of the same prompt — but do NOT bill anyone from it.
|
|
5
|
+
* class, built for comparing two versions of the same prompt — but do NOT bill
|
|
6
|
+
* anyone from it.
|
|
8
7
|
*
|
|
9
|
-
* **
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* **Its accuracy is not one number, and the years it was written as one were
|
|
9
|
+
* the mistake.** The branches below treat CJK, digits and punctuation quite
|
|
10
|
+
* differently from words, and there was never a reason those should land on the
|
|
11
|
+
* same accuracy. Measured over 47 samples against the official counting
|
|
12
|
+
* endpoint they do not: the worst error is 3.2% on CJK, 5.6% on Latin prose,
|
|
13
|
+
* 25.1% on code and quoting, and 32.5% on a digit-dominant table. `band.ts`
|
|
14
|
+
* holds the bands those measurements earn and `bandFor(text)` is what a caller
|
|
15
|
+
* should print; the constant below is only the widest of them, for callers that
|
|
16
|
+
* have no text in hand.
|
|
16
17
|
*
|
|
17
|
-
* `test/token-band.test.js`
|
|
18
|
-
*
|
|
18
|
+
* `test/token-band.test.js` holds every sample against the band `bandFor` gives
|
|
19
|
+
* it, and a sample edited since it was measured fails rather than passing
|
|
20
|
+
* quietly.
|
|
19
21
|
*
|
|
20
22
|
* For exact numbers use `countTokensAnthropic` (the official token-counting
|
|
21
23
|
* endpoint, which is free) or pass your own `TokenCounter`.
|
|
@@ -25,53 +27,47 @@ import { detectTextLanguage } from './language.js';
|
|
|
25
27
|
import { SAFE_FETCH_INIT, checkedEndpoint } from './net.js';
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
|
-
* The
|
|
30
|
+
* The widest band this estimator is published under, as a percentage.
|
|
29
31
|
*
|
|
30
|
-
* **
|
|
31
|
-
* releases as a design target nobody had checked; the first
|
|
32
|
-
* of eight samples outside it and it went to `25`; fixing
|
|
33
|
-
* calibrating per language brought it back to `15
|
|
34
|
-
*
|
|
32
|
+
* **A history worth keeping, because each value was believed at the time.** It
|
|
33
|
+
* was `15` for eight releases as a design target nobody had checked; the first
|
|
34
|
+
* measurement found two of eight samples outside it and it went to `25`; fixing
|
|
35
|
+
* the digit divisor and calibrating per language brought it back to `15`, and
|
|
36
|
+
* splitting kana from han — Japanese had been +11.2% and Chinese −3.2% under one
|
|
37
|
+
* rule — brought it to `10`. Four values, each an improvement, and all four
|
|
38
|
+
* wrong in the same way.
|
|
35
39
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
+
* **The corpus they were measured on was the corpus they were fitted to.**
|
|
41
|
+
* Twenty-one samples held thirteen files of Latin prose and exactly one each of
|
|
42
|
+
* code, numeric and punctuation, and those single files were where the constants
|
|
43
|
+
* came from. The worst error it could show was 6.4%, and 10 looked like a
|
|
44
|
+
* comfortable margin over it. Neither figure was about the estimator.
|
|
40
45
|
*
|
|
41
|
-
* **
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* safe direction for a tool that reports money.
|
|
46
|
+
* **It is no longer one number, and this one is the widest.** A corpus of
|
|
47
|
+
* thirteen prose files and one each of code, numeric and punctuation put the
|
|
48
|
+
* band at 10, and those single files were the set the constants had been fitted
|
|
49
|
+
* to. Measured on forty-seven, the same estimator is 6% out on prose and 33%
|
|
50
|
+
* out on a CSV ledger. `bandFor` in `band.ts` is what a caller that has the
|
|
51
|
+
* text should use.
|
|
48
52
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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.
|
|
53
|
+
* This stays for the callers that do not have it — a figure covering the whole
|
|
54
|
+
* catalogue of text, which can only be the worst of them. Widening it from 10
|
|
55
|
+
* to 33 makes every claim that reads it true where it had been false, at the
|
|
56
|
+
* cost of understating the estimator on prose. That trade is deliberate: a
|
|
57
|
+
* caller with no text cannot know which it is holding, and there is only one
|
|
58
|
+
* safe direction to guess in.
|
|
65
59
|
*/
|
|
66
|
-
export const ESTIMATE_ERROR_BAND_PCT =
|
|
60
|
+
export const ESTIMATE_ERROR_BAND_PCT = 33;
|
|
67
61
|
|
|
68
62
|
/**
|
|
69
63
|
* Whose tokenizer that band was measured against.
|
|
70
64
|
*
|
|
71
65
|
* The number above is not a property of the estimator; it is a property of the
|
|
72
|
-
* estimator **against Claude**.
|
|
73
|
-
* family. Trazum prices seven, and
|
|
74
|
-
*
|
|
66
|
+
* estimator **against Claude**. Forty-seven samples, one counting endpoint, one
|
|
67
|
+
* family. Trazum prices seven, and the two that have since been run are far
|
|
68
|
+
* outside anything published here: DeepSeek 94.5% at worst, Mistral 103.1%. On
|
|
69
|
+
* a GPT or a Gemini prompt nobody has measured it at all, and
|
|
70
|
+
* `measuredForeignError` answers null rather than the nearest number.
|
|
75
71
|
*
|
|
76
72
|
* Exported as a name rather than left implicit in a comment because the band
|
|
77
73
|
* had already leaked out of its domain in three places: an advisory that told a
|
|
@@ -173,6 +169,34 @@ const DIVISOR_BY_LANGUAGE: Readonly<Record<string, number>> = {
|
|
|
173
169
|
const KANA_TOKENS_PER_CHAR = 0.75;
|
|
174
170
|
const HAN_TOKENS_PER_CHAR = 1.05;
|
|
175
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Hangul, measured at last.
|
|
174
|
+
*
|
|
175
|
+
* The comment above used to end *"Hangul keeps the old cost of 1, because
|
|
176
|
+
* nothing here measures Korean. Guessing it from Japanese would be inventing a
|
|
177
|
+
* figure."* That was right, and it stayed right for exactly as long as nobody
|
|
178
|
+
* measured Korean. `cjk-korean.txt` now does: at 1 token per character the
|
|
179
|
+
* estimate came out **20.0% under** — 232 against 290 — and 1.20 takes it to
|
|
180
|
+
* -10.0%.
|
|
181
|
+
*
|
|
182
|
+
* **1.20 was the one-sample answer and it was wrong by ten points.** A second
|
|
183
|
+
* Korean sample in a different register — a deployment review against support
|
|
184
|
+
* copy — put both at -10.6% and -10.0% under it, and solving the constant on
|
|
185
|
+
* the two together lands on 1.35, where both come out at 0.0%.
|
|
186
|
+
*
|
|
187
|
+
* Two independent texts agreeing to within four tenths of a point across the
|
|
188
|
+
* whole search is what makes this a property of Hangul rather than a fit to a
|
|
189
|
+
* file. Their residuals move in lockstep at every candidate value: -10.6/-10.0
|
|
190
|
+
* at 1.20, -3.5/-3.1 at 1.30, 3.2/3.4 at 1.40. One sample cannot show that and
|
|
191
|
+
* two can, which is the entire argument for measuring more of the thin classes
|
|
192
|
+
* rather than tuning harder on the thin ones.
|
|
193
|
+
*
|
|
194
|
+
* Still in-sample: the honest test is a third Korean prompt, and the corpus
|
|
195
|
+
* grows one at a time.
|
|
196
|
+
*/
|
|
197
|
+
const HANGUL_TOKENS_PER_CHAR = 1.35;
|
|
198
|
+
const HANGUL = /[가-]/;
|
|
199
|
+
|
|
176
200
|
/**
|
|
177
201
|
* What a prompt gets when the language could not be told.
|
|
178
202
|
*
|
|
@@ -248,7 +272,11 @@ export function estimateTokens(text: string): number {
|
|
|
248
272
|
* where the loop happens to break, not of what the text costs.
|
|
249
273
|
*/
|
|
250
274
|
while (i < chars.length && CJK.test(chars[i]!)) {
|
|
251
|
-
cjkTokens += KANA.test(chars[i]!)
|
|
275
|
+
cjkTokens += KANA.test(chars[i]!)
|
|
276
|
+
? KANA_TOKENS_PER_CHAR
|
|
277
|
+
: HANGUL.test(chars[i]!)
|
|
278
|
+
? HANGUL_TOKENS_PER_CHAR
|
|
279
|
+
: HAN_TOKENS_PER_CHAR;
|
|
252
280
|
i++;
|
|
253
281
|
}
|
|
254
282
|
continue;
|
|
@@ -315,7 +343,29 @@ export function estimateTokens(text: string): number {
|
|
|
315
343
|
total += 1;
|
|
316
344
|
i++;
|
|
317
345
|
} else {
|
|
318
|
-
|
|
346
|
+
/**
|
|
347
|
+
* One token per symbol, not one per two.
|
|
348
|
+
*
|
|
349
|
+
* Two was the shipped value and every symbol-dense sample in the corpus
|
|
350
|
+
* undercounted under it, in the same direction, by amounts that grow with
|
|
351
|
+
* density: punctuation-heavy -5.7%, code-heavy -6.4%,
|
|
352
|
+
* punctuation-markup -11.4%, code-sql -25.6%, code-shell -29.8%. Five
|
|
353
|
+
* samples agreeing on a direction is a shape, not a fit — a merge table
|
|
354
|
+
* covers `, ` and `. ` and covers `${`, `"$`, `--` and `‑` far less
|
|
355
|
+
* well, and shell and SQL are made of the second kind.
|
|
356
|
+
*
|
|
357
|
+
* Searched over the whole corpus rather than the class it helps, so a
|
|
358
|
+
* value that fixed code by wrecking prose could not look like a win: at
|
|
359
|
+
* 1.0 the samples outside the published band drop from five to three and
|
|
360
|
+
* the mean error from 6.6% to 5.1%, with no sample moved more than a
|
|
361
|
+
* point in the wrong direction.
|
|
362
|
+
*
|
|
363
|
+
* The search wanted to go below 1.0 and was not allowed to. Below one is
|
|
364
|
+
* more than a token per symbol, which may well be true of SQL — code-sql
|
|
365
|
+
* is still 24.9% under — but it is a claim about a class this corpus has
|
|
366
|
+
* two samples of, and two samples do not earn a constant.
|
|
367
|
+
*/
|
|
368
|
+
total += Math.ceil(n / 1);
|
|
319
369
|
}
|
|
320
370
|
}
|
|
321
371
|
|