@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.
@@ -0,0 +1,219 @@
1
+ import type { PricingCatalogue } from './pricing.js';
2
+ /**
3
+ * Where the money actually went, from calls that actually happened.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * Everything else in this package reads a **prompt file** and reasons about what
8
+ * it would cost. That is the smallest line item on most bills, and the gap is not
9
+ * small enough to argue about: measured on an ordinary support prompt, the
10
+ * deterministic rules recover about **1%** of the monthly figure, while output
11
+ * tokens alone were **87%** of it. A tool that reads `prompts/*.txt` cannot see
12
+ * retrieved context, conversation history, tool results or answers, and on a RAG
13
+ * or agent workload those are nearly the whole invoice.
14
+ *
15
+ * So this reads the other direction: **what the provider actually charged**, per
16
+ * call, and says where it went. The sentence it is built to produce is "63% of
17
+ * your bill is retrieved context and nothing is watching it", which is a fact
18
+ * about a system rather than an estimate about a file.
19
+ *
20
+ * ## It reads a file, and that is the design
21
+ *
22
+ * Not a proxy, not an SDK wrapper, not a callback. Trazum's whole security
23
+ * position is that prompts do not leave the machine they are on — asserted by
24
+ * tests, not promised — and a tool that sits in the request path trades that away
25
+ * for convenience. A JSON Lines file is something you already have or can produce
26
+ * in three lines, and it keeps the guarantee intact.
27
+ *
28
+ * ## The format is the one the API already gives you
29
+ *
30
+ * Nothing is invented here. Every Anthropic response carries a `usage` object
31
+ * with exactly these fields, so recording a call is:
32
+ *
33
+ * ```ts
34
+ * appendFileSync('usage.jsonl', JSON.stringify({
35
+ * model: response.model,
36
+ * ...response.usage,
37
+ * }) + '\n');
38
+ * ```
39
+ *
40
+ * OpenAI's `usage` maps onto the same shape with different names, and
41
+ * `parseUsageLine` accepts both. Asking somebody to transform their logs into a
42
+ * bespoke schema before a tool will read them is how a tool goes unused.
43
+ *
44
+ * ## What it refuses to do
45
+ *
46
+ * **It does not read prompt text and there is nowhere to put it.** The record
47
+ * shape has no field for content, so a usage log handed to Trazum cannot contain
48
+ * a prompt even by accident. That is a stronger promise than "we do not look at
49
+ * it", and it is the reason this takes counts rather than calls.
50
+ *
51
+ * **It reports no saving.** Attributing "you could have saved X" to a call that
52
+ * already happened means guessing what the call should have been, and this module
53
+ * exists precisely because guessing is what the rest of the package has to do.
54
+ * It reports what was spent, split by where it went. What to do about it is a
55
+ * different question and belongs to the advisories.
56
+ */
57
+ /** One recorded call, after parsing. All counts, no content. */
58
+ export interface UsageRecord {
59
+ /** Model id as the provider reported it. */
60
+ model: string;
61
+ /** Uncached input tokens billed at the full rate. */
62
+ inputTokens: number;
63
+ /** Tokens billed at the cache-read rate. Zero when nothing was cached. */
64
+ cacheReadTokens: number;
65
+ /** Cache writes at the 5-minute rate — 1.25x input on Anthropic. */
66
+ cacheWrite5mTokens: number;
67
+ /** Cache writes at the 1-hour rate, which is **2x** input, not 1.25x. */
68
+ cacheWrite1hTokens: number;
69
+ /**
70
+ * Whether the log said which TTL those writes used.
71
+ *
72
+ * `false` when only the flat `cache_creation_input_tokens` was present and it
73
+ * was non-zero: the writes are then priced at the cheaper 5-minute rate because
74
+ * one of the two has to be assumed, and the report says so. Choosing the cheaper
75
+ * rate silently understates a 1-hour workload by 37.5% on its largest line.
76
+ */
77
+ writeTtlKnown: boolean;
78
+ outputTokens: number;
79
+ /**
80
+ * Optional label for grouping — an endpoint, a feature, a prompt name.
81
+ *
82
+ * The whole value of a profile is answering "which part of the product costs
83
+ * this", and without a label every call looks alike. Unlabelled records are
84
+ * grouped under a single bucket rather than dropped, because a profile that
85
+ * refuses to read a log until it is annotated is a profile nobody runs.
86
+ */
87
+ label: string | null;
88
+ }
89
+ /** What a set of calls cost, split by where the money went. */
90
+ export interface UsageBreakdown {
91
+ calls: number;
92
+ inputTokens: number;
93
+ cacheReadTokens: number;
94
+ cacheWriteTokens: number;
95
+ outputTokens: number;
96
+ /**
97
+ * Calls whose cache-write TTL the log did not state, so the cheaper rate was
98
+ * assumed. Non-zero means this total is a floor on those calls, not a figure.
99
+ */
100
+ assumedWriteTtlCalls: number;
101
+ inputUsd: number;
102
+ cacheReadUsd: number;
103
+ cacheWriteUsd: number;
104
+ outputUsd: number;
105
+ totalUsd: number;
106
+ }
107
+ export interface UsageProfileReport {
108
+ /** Everything, combined. */
109
+ total: UsageBreakdown;
110
+ /** Per `label`, largest bill first — the order somebody would act in. */
111
+ byLabel: Array<{
112
+ label: string;
113
+ breakdown: UsageBreakdown;
114
+ }>;
115
+ /** Per model, largest bill first. */
116
+ byModel: Array<{
117
+ model: string;
118
+ breakdown: UsageBreakdown;
119
+ }>;
120
+ /**
121
+ * Models in the log that the pricing catalogue does not know.
122
+ *
123
+ * Named rather than silently costed at zero. A profile that quietly omits a
124
+ * model reports a total lower than the real bill, which is the flattering
125
+ * direction and the one this repository refuses.
126
+ */
127
+ unpricedModels: string[];
128
+ /**
129
+ * What those models used, kept entirely out of `total`.
130
+ *
131
+ * The first version added their **tokens** to the totals and their **dollars**
132
+ * to nothing, because pricing failed after the counts had been accumulated. So
133
+ * `total.inputTokens` included them and `total.inputUsd` did not, and anybody
134
+ * dividing one by the other got a cost per token that was wrong by however much
135
+ * of the log was unpriced — silently, and low.
136
+ *
137
+ * They are separated now. `total` is what could be priced, tokens and dollars
138
+ * describing the same calls. This is what could not, so the size of the gap is
139
+ * visible instead of being folded into a number that looks complete.
140
+ */
141
+ unpriced: UsageBreakdown;
142
+ /**
143
+ * Lines that could not be read, with their 1-based position.
144
+ *
145
+ * Reported rather than thrown on. A log with three malformed lines out of forty
146
+ * thousand should still produce a profile, and a parser that dies on the first
147
+ * one makes the tool unusable on real data — but a parser that skips quietly
148
+ * produces a total that is wrong by an unknown amount.
149
+ */
150
+ skippedLines: number[];
151
+ }
152
+ /** The share of the bill each part accounts for, as fractions of 1. */
153
+ export interface UsageShares {
154
+ input: number;
155
+ cacheRead: number;
156
+ cacheWrite: number;
157
+ output: number;
158
+ }
159
+ /**
160
+ * One line of a usage log, or `null` when it is not one.
161
+ *
162
+ * Accepts the Anthropic shape and the OpenAI one, because those are the two
163
+ * things people actually have. The alternative — a Trazum-specific schema — asks
164
+ * for a transformation step before the tool will read anything, and a tool with a
165
+ * setup cost that exceeds its payoff does not get run twice.
166
+ *
167
+ * `null` in three cases, and the third is the one that was wrong:
168
+ *
169
+ * 1. Not JSON, or not an object, or no `model`.
170
+ * 2. **No** token counts at all — counting it would inflate the call count while
171
+ * contributing nothing, which lowers every per-call figure.
172
+ * 3. **Any** count present but unreadable. A field that is there and unusable is
173
+ * corruption, and a corrupt line belongs in `skippedLines` where the report
174
+ * names it, not in the totals as a silent zero.
175
+ */
176
+ export declare function parseUsageLine(line: string): UsageRecord | null;
177
+ /** The bucket unlabelled calls land in, named so a report can say so. */
178
+ export declare const UNLABELLED = "unlabelled";
179
+ export interface UsageProfileOptions {
180
+ catalogue: PricingCatalogue;
181
+ /** Date the prices are read at, so a promotional rate resolves the same way. */
182
+ on?: Date;
183
+ }
184
+ /**
185
+ * Reads a usage log and says where the money went.
186
+ *
187
+ * Takes the whole text rather than a stream: a usage log is measured in megabytes
188
+ * and this package imports no Node builtins, so streaming would mean an interface
189
+ * the browser build cannot satisfy. `@trazum/core/node` is where file reading
190
+ * lives, and it can chunk if it ever needs to.
191
+ */
192
+ export declare function profileUsage(text: string, options: UsageProfileOptions): UsageProfileReport;
193
+ /**
194
+ * What share of the bill each part is.
195
+ *
196
+ * The point of the whole module in one function: a caller can print "output is
197
+ * 87% of this" without doing arithmetic that would drift from the arithmetic
198
+ * here.
199
+ *
200
+ * All zeroes when nothing was spent, rather than `NaN`. A profile of an empty log
201
+ * is a legitimate result — no calls yet — and a report full of `NaN%` is a bug
202
+ * report from somebody who did nothing wrong.
203
+ */
204
+ export declare function sharesOf(breakdown: UsageBreakdown): UsageShares;
205
+ /**
206
+ * How much of the input that could have been cached was.
207
+ *
208
+ * `null` when nothing was cacheable-looking at all — no reads and no writes —
209
+ * because a hit rate over zero attempts is not zero, it is undefined, and
210
+ * printing "0% cache hit rate" for somebody who never turned caching on is a
211
+ * finding about nothing.
212
+ *
213
+ * Reads against reads-plus-full-price-input, deliberately. Cache *writes* are
214
+ * excluded from the denominator: a write is the cost of establishing an entry,
215
+ * not a missed read, and counting it as a miss makes a healthy cache look broken
216
+ * on the day it warms.
217
+ */
218
+ export declare function cacheHitRate(breakdown: UsageBreakdown): number | null;
219
+ //# sourceMappingURL=usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,gEAAgE;AAChE,MAAM,WAAW,WAAW;IAC1B,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,eAAe,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;;;;OAOG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,4BAA4B;IAC5B,KAAK,EAAE,cAAc,CAAC;IACtB,yEAAyE;IACzE,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,cAAc,CAAA;KAAE,CAAC,CAAC;IAC7D,qCAAqC;IACrC,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,cAAc,CAAA;KAAE,CAAC,CAAC;IAC7D;;;;;;OAMG;IACH,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB;;;;;;;;;;;;OAYG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;;;;OAOG;IACH,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAoDD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAuF/D;AAED,yEAAyE;AACzE,eAAO,MAAM,UAAU,eAAe,CAAC;AAiDvC,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,gFAAgF;IAChF,EAAE,CAAC,EAAE,IAAI,CAAC;CACX;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CAyD3F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,cAAc,GAAG,WAAW,CAS/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAKrE"}
package/dist/usage.js ADDED
@@ -0,0 +1,274 @@
1
+ import { effectivePricing, multipliersFor } from './pricing.js';
2
+ const EMPTY = () => ({
3
+ calls: 0,
4
+ inputTokens: 0,
5
+ cacheReadTokens: 0,
6
+ cacheWriteTokens: 0,
7
+ outputTokens: 0,
8
+ assumedWriteTtlCalls: 0,
9
+ inputUsd: 0,
10
+ cacheReadUsd: 0,
11
+ cacheWriteUsd: 0,
12
+ outputUsd: 0,
13
+ totalUsd: 0,
14
+ });
15
+ const OK = (value) => ({ kind: 'ok', value });
16
+ function readCount(...candidates) {
17
+ let sawCorrupt = false;
18
+ for (const value of candidates) {
19
+ if (value === undefined)
20
+ continue;
21
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
22
+ return OK(value);
23
+ // Present and unusable: a string, a null, a negative, a NaN.
24
+ sawCorrupt = true;
25
+ }
26
+ return sawCorrupt ? { kind: 'corrupt' } : { kind: 'absent' };
27
+ }
28
+ /** Zero for an absent count. Callers reject corrupt ones before reaching this. */
29
+ const valueOf = (count) => (count.kind === 'ok' ? count.value : 0);
30
+ /**
31
+ * One line of a usage log, or `null` when it is not one.
32
+ *
33
+ * Accepts the Anthropic shape and the OpenAI one, because those are the two
34
+ * things people actually have. The alternative — a Trazum-specific schema — asks
35
+ * for a transformation step before the tool will read anything, and a tool with a
36
+ * setup cost that exceeds its payoff does not get run twice.
37
+ *
38
+ * `null` in three cases, and the third is the one that was wrong:
39
+ *
40
+ * 1. Not JSON, or not an object, or no `model`.
41
+ * 2. **No** token counts at all — counting it would inflate the call count while
42
+ * contributing nothing, which lowers every per-call figure.
43
+ * 3. **Any** count present but unreadable. A field that is there and unusable is
44
+ * corruption, and a corrupt line belongs in `skippedLines` where the report
45
+ * names it, not in the totals as a silent zero.
46
+ */
47
+ export function parseUsageLine(line) {
48
+ let raw;
49
+ try {
50
+ raw = JSON.parse(line);
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
56
+ return null;
57
+ const record = raw;
58
+ // Anthropic nests usage on a response; a hand-rolled log usually flattens it.
59
+ const usage = typeof record.usage === 'object' && record.usage !== null
60
+ ? record.usage
61
+ : record;
62
+ const model = typeof record.model === 'string' ? record.model : null;
63
+ if (!model)
64
+ return null;
65
+ /**
66
+ * OpenAI reports cached tokens inside `prompt_tokens_details` **and counts them
67
+ * in `prompt_tokens`**, while Anthropic reports them separately and does not.
68
+ * Subtracting in one case and not the other is the difference between a correct
69
+ * bill and one that charges the cached half twice.
70
+ */
71
+ const details = typeof usage.prompt_tokens_details === 'object' && usage.prompt_tokens_details !== null
72
+ ? usage.prompt_tokens_details
73
+ : null;
74
+ const openAiCached = details ? readCount(details.cached_tokens) : { kind: 'absent' };
75
+ /**
76
+ * Anthropic splits cache writes by time-to-live, and the two cost different
77
+ * amounts: 1.25x input for the 5-minute entry, **2x** for the 1-hour one.
78
+ *
79
+ * Reading only the flat `cache_creation_input_tokens` threw that distinction
80
+ * away and then priced everything at the cheaper rate — a 1-hour workload
81
+ * reported 37.5% under, silently, on its largest line. The split is in the log
82
+ * whenever the recording recipe in the README is followed, because it is part of
83
+ * the `usage` object the API returns.
84
+ */
85
+ const creation = typeof usage.cache_creation === 'object' && usage.cache_creation !== null
86
+ ? usage.cache_creation
87
+ : null;
88
+ const write5m = creation ? readCount(creation.ephemeral_5m_input_tokens) : { kind: 'absent' };
89
+ const write1h = creation ? readCount(creation.ephemeral_1h_input_tokens) : { kind: 'absent' };
90
+ const counts = {
91
+ input: readCount(usage.input_tokens, usage.inputTokens, usage.prompt_tokens),
92
+ output: readCount(usage.output_tokens, usage.outputTokens, usage.completion_tokens),
93
+ cacheRead: readCount(usage.cache_read_input_tokens, usage.cacheReadTokens),
94
+ cacheWrite: readCount(usage.cache_creation_input_tokens, usage.cacheWriteTokens),
95
+ openAiCached,
96
+ write5m,
97
+ write1h,
98
+ };
99
+ // Any field present and unreadable rejects the line. See `readCount`.
100
+ if (Object.values(counts).some((c) => c.kind === 'corrupt'))
101
+ return null;
102
+ // Nothing to count at all.
103
+ if (Object.values(counts).every((c) => c.kind === 'absent'))
104
+ return null;
105
+ const cached = valueOf(counts.openAiCached);
106
+ const flatWrite = valueOf(counts.cacheWrite);
107
+ const split5m = valueOf(counts.write5m);
108
+ const split1h = valueOf(counts.write1h);
109
+ const hasSplit = counts.write5m.kind === 'ok' || counts.write1h.kind === 'ok';
110
+ return {
111
+ model,
112
+ inputTokens: Math.max(0, valueOf(counts.input) - cached),
113
+ cacheReadTokens: counts.cacheRead.kind === 'ok' ? counts.cacheRead.value : cached,
114
+ /**
115
+ * The split when the log carries it, the flat number otherwise — and
116
+ * `writeTtlKnown` says which, so the report can admit that a rate was assumed
117
+ * rather than quietly choosing the cheaper one.
118
+ */
119
+ cacheWrite5mTokens: hasSplit ? split5m : flatWrite,
120
+ cacheWrite1hTokens: hasSplit ? split1h : 0,
121
+ writeTtlKnown: hasSplit || flatWrite === 0,
122
+ outputTokens: valueOf(counts.output),
123
+ label: typeof record.label === 'string' && record.label.trim() !== ''
124
+ ? record.label.trim()
125
+ : null,
126
+ };
127
+ }
128
+ /** The bucket unlabelled calls land in, named so a report can say so. */
129
+ export const UNLABELLED = 'unlabelled';
130
+ /** Token counts only. Used for both halves, because both need them. */
131
+ function countInto(into, record) {
132
+ into.calls += 1;
133
+ into.inputTokens += record.inputTokens;
134
+ into.cacheReadTokens += record.cacheReadTokens;
135
+ into.cacheWriteTokens += record.cacheWrite5mTokens + record.cacheWrite1hTokens;
136
+ if (!record.writeTtlKnown)
137
+ into.assumedWriteTtlCalls += 1;
138
+ into.outputTokens += record.outputTokens;
139
+ }
140
+ function add(into, record, catalogue, on) {
141
+ /**
142
+ * Looked up directly rather than through `modelFrom`, which **throws** on an id
143
+ * it does not know. A usage log is somebody's production traffic and will
144
+ * contain models this catalogue has never heard of — a fine-tune, a preview, a
145
+ * competitor. Throwing means one unfamiliar id destroys the whole profile;
146
+ * naming it separately means the report is honest about what it could not price
147
+ * and useful about everything else.
148
+ *
149
+ * **Priced first, counted second.** The other order was the bug: counts landed
150
+ * before the lookup could fail, so an unpriced call contributed tokens to a
151
+ * total whose dollars excluded it.
152
+ */
153
+ const model = catalogue.byId.get(record.model);
154
+ if (!model)
155
+ return false;
156
+ countInto(into, record);
157
+ const { inputPerMTok, outputPerMTok } = effectivePricing(model, on);
158
+ const rates = multipliersFor(model);
159
+ const per = (tokens, rate) => (tokens / 1_000_000) * rate;
160
+ into.inputUsd += per(record.inputTokens, inputPerMTok);
161
+ into.cacheReadUsd += per(record.cacheReadTokens, inputPerMTok * rates.cacheRead);
162
+ /**
163
+ * Each TTL at its own rate. Anthropic charges 1.25x input for a 5-minute entry
164
+ * and 2x for a 1-hour one, and the first version applied 1.25x to both — 37.5%
165
+ * under on a 1-hour workload, on the largest line, with nothing on screen
166
+ * saying a rate had been chosen.
167
+ */
168
+ into.cacheWriteUsd += per(record.cacheWrite5mTokens, inputPerMTok * rates.cacheWrite5m);
169
+ into.cacheWriteUsd += per(record.cacheWrite1hTokens, inputPerMTok * rates.cacheWrite1h);
170
+ into.outputUsd += per(record.outputTokens, outputPerMTok);
171
+ into.totalUsd =
172
+ into.inputUsd + into.cacheReadUsd + into.cacheWriteUsd + into.outputUsd;
173
+ return true;
174
+ }
175
+ /**
176
+ * Reads a usage log and says where the money went.
177
+ *
178
+ * Takes the whole text rather than a stream: a usage log is measured in megabytes
179
+ * and this package imports no Node builtins, so streaming would mean an interface
180
+ * the browser build cannot satisfy. `@trazum/core/node` is where file reading
181
+ * lives, and it can chunk if it ever needs to.
182
+ */
183
+ export function profileUsage(text, options) {
184
+ const { catalogue, on = new Date() } = options;
185
+ const total = EMPTY();
186
+ const unpriced = EMPTY();
187
+ const byLabel = new Map();
188
+ const byModel = new Map();
189
+ const unpricedModels = new Set();
190
+ const skippedLines = [];
191
+ const lines = text.split('\n');
192
+ for (let i = 0; i < lines.length; i += 1) {
193
+ const line = lines[i].trim();
194
+ if (line === '')
195
+ continue;
196
+ const record = parseUsageLine(line);
197
+ if (!record) {
198
+ skippedLines.push(i + 1);
199
+ continue;
200
+ }
201
+ if (!add(total, record, catalogue, on)) {
202
+ unpricedModels.add(record.model);
203
+ countInto(unpriced, record);
204
+ // Still grouped by model, so the reader can see which unknown id is costing
205
+ // them attention — but with zero dollars, which the grouping makes obvious.
206
+ if (!byModel.has(record.model))
207
+ byModel.set(record.model, EMPTY());
208
+ countInto(byModel.get(record.model), record);
209
+ continue;
210
+ }
211
+ const labelKey = record.label ?? UNLABELLED;
212
+ if (!byLabel.has(labelKey))
213
+ byLabel.set(labelKey, EMPTY());
214
+ add(byLabel.get(labelKey), record, catalogue, on);
215
+ if (!byModel.has(record.model))
216
+ byModel.set(record.model, EMPTY());
217
+ add(byModel.get(record.model), record, catalogue, on);
218
+ }
219
+ const sorted = (map, key) => [...map.entries()]
220
+ .sort((a, b) => b[1].totalUsd - a[1].totalUsd || a[0].localeCompare(b[0]))
221
+ .map(([name, breakdown]) => ({ [key]: name, breakdown }));
222
+ return {
223
+ total,
224
+ byLabel: sorted(byLabel, 'label'),
225
+ byModel: sorted(byModel, 'model'),
226
+ unpricedModels: [...unpricedModels].sort(),
227
+ unpriced,
228
+ skippedLines,
229
+ };
230
+ }
231
+ /**
232
+ * What share of the bill each part is.
233
+ *
234
+ * The point of the whole module in one function: a caller can print "output is
235
+ * 87% of this" without doing arithmetic that would drift from the arithmetic
236
+ * here.
237
+ *
238
+ * All zeroes when nothing was spent, rather than `NaN`. A profile of an empty log
239
+ * is a legitimate result — no calls yet — and a report full of `NaN%` is a bug
240
+ * report from somebody who did nothing wrong.
241
+ */
242
+ export function sharesOf(breakdown) {
243
+ const { totalUsd } = breakdown;
244
+ if (totalUsd <= 0)
245
+ return { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
246
+ return {
247
+ input: breakdown.inputUsd / totalUsd,
248
+ cacheRead: breakdown.cacheReadUsd / totalUsd,
249
+ cacheWrite: breakdown.cacheWriteUsd / totalUsd,
250
+ output: breakdown.outputUsd / totalUsd,
251
+ };
252
+ }
253
+ /**
254
+ * How much of the input that could have been cached was.
255
+ *
256
+ * `null` when nothing was cacheable-looking at all — no reads and no writes —
257
+ * because a hit rate over zero attempts is not zero, it is undefined, and
258
+ * printing "0% cache hit rate" for somebody who never turned caching on is a
259
+ * finding about nothing.
260
+ *
261
+ * Reads against reads-plus-full-price-input, deliberately. Cache *writes* are
262
+ * excluded from the denominator: a write is the cost of establishing an entry,
263
+ * not a missed read, and counting it as a miss makes a healthy cache look broken
264
+ * on the day it warms.
265
+ */
266
+ export function cacheHitRate(breakdown) {
267
+ const attempts = breakdown.cacheReadTokens + breakdown.inputTokens;
268
+ if (breakdown.cacheReadTokens === 0 && breakdown.cacheWriteTokens === 0)
269
+ return null;
270
+ if (attempts === 0)
271
+ return null;
272
+ return breakdown.cacheReadTokens / attempts;
273
+ }
274
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trazum/core",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Trazum core: priced advisories for LLM prompts (caching, model tier, batching, schemas), plus deterministic trimming, token counting and pricing.",
5
5
  "license": "MIT",
6
6
  "author": "David Mu\u00f1oz Rey",