@trazum/core 1.10.0 → 1.26.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/dist/against.d.ts +32 -0
- package/dist/against.d.ts.map +1 -0
- package/dist/against.js +34 -0
- package/dist/against.js.map +1 -0
- package/dist/config-schema.d.ts +42 -1
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +61 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/conversation.d.ts +121 -0
- package/dist/conversation.d.ts.map +1 -0
- package/dist/conversation.js +157 -0
- package/dist/conversation.js.map +1 -0
- package/dist/csv.d.ts +61 -0
- package/dist/csv.d.ts.map +1 -0
- package/dist/csv.js +149 -0
- package/dist/csv.js.map +1 -0
- package/dist/evaluate.d.ts +24 -0
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +5 -2
- package/dist/evaluate.js.map +1 -1
- package/dist/index.d.ts +25 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/input-shape.d.ts +104 -0
- package/dist/input-shape.d.ts.map +1 -0
- package/dist/input-shape.js +132 -0
- package/dist/input-shape.js.map +1 -0
- package/dist/levers.d.ts +151 -0
- package/dist/levers.d.ts.map +1 -0
- package/dist/levers.js +160 -0
- package/dist/levers.js.map +1 -0
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/output-shape.d.ts +96 -0
- package/dist/output-shape.d.ts.map +1 -0
- package/dist/output-shape.js +145 -0
- package/dist/output-shape.js.map +1 -0
- package/dist/pricing-overlay.d.ts +1 -1
- package/dist/pricing-overlay.d.ts.map +1 -1
- package/dist/pricing-overlay.js +46 -0
- package/dist/pricing-overlay.js.map +1 -1
- package/dist/repeats.d.ts +75 -0
- package/dist/repeats.d.ts.map +1 -0
- package/dist/repeats.js +82 -0
- package/dist/repeats.js.map +1 -0
- package/dist/reprice.d.ts +143 -0
- package/dist/reprice.d.ts.map +1 -0
- package/dist/reprice.js +82 -0
- package/dist/reprice.js.map +1 -0
- package/dist/session-cost.d.ts +70 -0
- package/dist/session-cost.d.ts.map +1 -0
- package/dist/session-cost.js +90 -0
- package/dist/session-cost.js.map +1 -0
- package/dist/session-ledger.d.ts +77 -0
- package/dist/session-ledger.d.ts.map +1 -0
- package/dist/session-ledger.js +99 -0
- package/dist/session-ledger.js.map +1 -0
- package/dist/ttl-fit.d.ts +103 -0
- package/dist/ttl-fit.d.ts.map +1 -0
- package/dist/ttl-fit.js +184 -0
- package/dist/ttl-fit.js.map +1 -0
- package/dist/usage.d.ts +434 -16
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +383 -23
- package/dist/usage.js.map +1 -1
- package/package.json +1 -1
- package/src/against.ts +48 -0
- package/src/config-schema.ts +106 -0
- package/src/conversation.ts +305 -0
- package/src/csv.ts +184 -0
- package/src/evaluate.ts +33 -3
- package/src/index.ts +51 -1
- package/src/input-shape.ts +259 -0
- package/src/levers.ts +331 -0
- package/src/node.ts +1 -1
- package/src/output-shape.ts +254 -0
- package/src/pricing-overlay.ts +52 -1
- package/src/repeats.ts +166 -0
- package/src/reprice.ts +227 -0
- package/src/session-cost.ts +170 -0
- package/src/session-ledger.ts +189 -0
- package/src/ttl-fit.ts +251 -0
- package/src/usage.ts +795 -7
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
2
|
+
import { UNLABELLED } from './usage.js';
|
|
3
|
+
/**
|
|
4
|
+
* Bucket edges sized for requests rather than answers.
|
|
5
|
+
*
|
|
6
|
+
* 512 tokens up to 65,536 is finer than any decision about a prompt, and past
|
|
7
|
+
* that the buckets widen to 8,192: the difference between a 400,000-token
|
|
8
|
+
* request and a 404,000-token one changes nothing. The last bucket is
|
|
9
|
+
* open-ended so a call larger than the widest edge still lands somewhere,
|
|
10
|
+
* counted rather than dropped.
|
|
11
|
+
*/
|
|
12
|
+
const SMALL_STEP = 512;
|
|
13
|
+
const SMALL_LIMIT = 65_536;
|
|
14
|
+
const LARGE_STEP = 8_192;
|
|
15
|
+
const LARGE_LIMIT = 1_048_576;
|
|
16
|
+
const EDGES = (() => {
|
|
17
|
+
const edges = [];
|
|
18
|
+
for (let t = 0; t < SMALL_LIMIT; t += SMALL_STEP)
|
|
19
|
+
edges.push(t);
|
|
20
|
+
for (let t = SMALL_LIMIT; t < LARGE_LIMIT; t += LARGE_STEP)
|
|
21
|
+
edges.push(t);
|
|
22
|
+
return edges;
|
|
23
|
+
})();
|
|
24
|
+
const SMALL_BUCKETS = SMALL_LIMIT / SMALL_STEP;
|
|
25
|
+
/** Index of the bucket a count falls in. The last bucket is open-ended. */
|
|
26
|
+
function bucketOf(tokens) {
|
|
27
|
+
if (tokens >= EDGES[EDGES.length - 1])
|
|
28
|
+
return EDGES.length - 1;
|
|
29
|
+
if (tokens < SMALL_LIMIT)
|
|
30
|
+
return Math.floor(tokens / SMALL_STEP);
|
|
31
|
+
return SMALL_BUCKETS + Math.floor((tokens - SMALL_LIMIT) / LARGE_STEP);
|
|
32
|
+
}
|
|
33
|
+
/** A bucket's upper edge, or `null` for the open-ended last one. */
|
|
34
|
+
function upperEdgeOf(bucket) {
|
|
35
|
+
if (bucket >= EDGES.length - 1)
|
|
36
|
+
return null;
|
|
37
|
+
return EDGES[bucket + 1];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The bucket ceiling covering `share` of the calls, walking up from the
|
|
41
|
+
* smallest requests. Exact over the histogram: every call at or below the
|
|
42
|
+
* returned ceiling is counted, none is interpolated.
|
|
43
|
+
*/
|
|
44
|
+
function ceilingFor(buckets, totalCalls, share) {
|
|
45
|
+
const ascending = [...buckets.keys()].sort((a, b) => a - b);
|
|
46
|
+
const target = totalCalls * share;
|
|
47
|
+
let covered = 0;
|
|
48
|
+
for (const b of ascending) {
|
|
49
|
+
covered += buckets.get(b);
|
|
50
|
+
if (covered >= target)
|
|
51
|
+
return upperEdgeOf(b);
|
|
52
|
+
}
|
|
53
|
+
return upperEdgeOf(ascending[ascending.length - 1]);
|
|
54
|
+
}
|
|
55
|
+
/** An accumulator, fed in the pass a profile already makes. */
|
|
56
|
+
export function createInputShapeTracker(options) {
|
|
57
|
+
const { catalogue, on = new Date(), minShare = 0.05, minCalls = 20 } = options;
|
|
58
|
+
const slices = new Map();
|
|
59
|
+
const add = (record) => {
|
|
60
|
+
const model = catalogue.byId.get(record.model);
|
|
61
|
+
// An unpriced model contributes no dollars anywhere else; a shape drawn
|
|
62
|
+
// from one would describe a bill that was never computed.
|
|
63
|
+
if (!model)
|
|
64
|
+
return;
|
|
65
|
+
const tokens = record.inputTokens +
|
|
66
|
+
record.cacheReadTokens +
|
|
67
|
+
record.cacheWrite5mTokens +
|
|
68
|
+
record.cacheWrite1hTokens;
|
|
69
|
+
if (tokens <= 0)
|
|
70
|
+
return;
|
|
71
|
+
const key = `${record.label ?? UNLABELLED}\n${record.model}`;
|
|
72
|
+
let slice = slices.get(key);
|
|
73
|
+
if (!slice) {
|
|
74
|
+
slice = { calls: 0, inputTokens: 0, cachedTokens: 0, inputUsd: 0, buckets: new Map() };
|
|
75
|
+
slices.set(key, slice);
|
|
76
|
+
}
|
|
77
|
+
const { inputPerMTok } = effectivePricing(model, on);
|
|
78
|
+
const rates = multipliersFor(model);
|
|
79
|
+
const per = (count, rate) => (count / 1_000_000) * rate;
|
|
80
|
+
slice.calls += 1;
|
|
81
|
+
slice.inputTokens += tokens;
|
|
82
|
+
slice.cachedTokens += record.cacheReadTokens;
|
|
83
|
+
slice.inputUsd +=
|
|
84
|
+
per(record.inputTokens, inputPerMTok) +
|
|
85
|
+
per(record.cacheReadTokens, inputPerMTok * rates.cacheRead) +
|
|
86
|
+
per(record.cacheWrite5mTokens, inputPerMTok * rates.cacheWrite5m) +
|
|
87
|
+
per(record.cacheWrite1hTokens, inputPerMTok * rates.cacheWrite1h);
|
|
88
|
+
const b = bucketOf(tokens);
|
|
89
|
+
slice.buckets.set(b, (slice.buckets.get(b) ?? 0) + 1);
|
|
90
|
+
};
|
|
91
|
+
const finish = (totalUsd) => {
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const [key, slice] of slices) {
|
|
94
|
+
const split = key.indexOf('\n');
|
|
95
|
+
const label = key.slice(0, split);
|
|
96
|
+
const modelId = key.slice(split + 1);
|
|
97
|
+
const model = catalogue.byId.get(modelId);
|
|
98
|
+
if (!model || slice.calls < minCalls)
|
|
99
|
+
continue;
|
|
100
|
+
const shareOfBill = totalUsd > 0 ? slice.inputUsd / totalUsd : 0;
|
|
101
|
+
if (shareOfBill < minShare)
|
|
102
|
+
continue;
|
|
103
|
+
const medianWithinTokens = ceilingFor(slice.buckets, slice.calls, 0.5);
|
|
104
|
+
const p95WithinTokens = ceilingFor(slice.buckets, slice.calls, 0.95);
|
|
105
|
+
out.push({
|
|
106
|
+
label,
|
|
107
|
+
model: modelId,
|
|
108
|
+
modelName: model.displayName,
|
|
109
|
+
calls: slice.calls,
|
|
110
|
+
inputTokens: slice.inputTokens,
|
|
111
|
+
inputUsd: slice.inputUsd,
|
|
112
|
+
shareOfBill,
|
|
113
|
+
medianWithinTokens,
|
|
114
|
+
p95WithinTokens,
|
|
115
|
+
p95OverMedian: medianWithinTokens !== null && p95WithinTokens !== null && medianWithinTokens > 0
|
|
116
|
+
? p95WithinTokens / medianWithinTokens
|
|
117
|
+
: null,
|
|
118
|
+
cachedShare: slice.inputTokens > 0 ? slice.cachedTokens / slice.inputTokens : 0,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return out.sort((a, b) => b.inputUsd - a.inputUsd);
|
|
122
|
+
};
|
|
123
|
+
return { add, finish };
|
|
124
|
+
}
|
|
125
|
+
/** The same measurement over a list of records, for a caller holding one. */
|
|
126
|
+
export function inputShapes(records, totalUsd, options) {
|
|
127
|
+
const tracker = createInputShapeTracker(options);
|
|
128
|
+
for (const record of records)
|
|
129
|
+
tracker.add(record);
|
|
130
|
+
return tracker.finish(totalUsd);
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=input-shape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-shape.js","sourceRoot":"","sources":["../src/input-shape.ts"],"names":[],"mappings":"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"}
|
package/dist/levers.d.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { UNLABELLED } from './usage.js';
|
|
2
|
+
import type { PricingCatalogue } from './pricing.js';
|
|
3
|
+
import type { UsageProfileReport } from './usage.js';
|
|
4
|
+
/**
|
|
5
|
+
* What would actually move this bill.
|
|
6
|
+
*
|
|
7
|
+
* ## The number this exists to answer
|
|
8
|
+
*
|
|
9
|
+
* Trazum's rules recover about **1%** of a bill. Measured, on an ordinary support
|
|
10
|
+
* prompt: three tokens out of three hundred and six. On a company spending twenty
|
|
11
|
+
* thousand a month that is two hundred, and nobody installs a tool for two
|
|
12
|
+
* hundred. The complaint is correct and it is the most important thing anybody has
|
|
13
|
+
* said about this product.
|
|
14
|
+
*
|
|
15
|
+
* The rest of the package reads a prompt file and shortens it. This reads what was
|
|
16
|
+
* charged and prices the levers that are **not** the prompt, from the same log, at
|
|
17
|
+
* the same arithmetic:
|
|
18
|
+
*
|
|
19
|
+
* | lever | what it moves |
|
|
20
|
+
* |---|---|
|
|
21
|
+
* | which model the call goes to | Opus 5 → Sonnet 5 is 40% off; → Haiku 4.5 is 80% |
|
|
22
|
+
* | the Batch API | 50% flat, on input and output |
|
|
23
|
+
* | prompt caching | 3–4x the rules |
|
|
24
|
+
* | shortening the prompt | ~1% |
|
|
25
|
+
*
|
|
26
|
+
* So the honest headline is that **the money is in what you call, not in how long
|
|
27
|
+
* the prompt is** — and the tool that only did the last row should say so, in the
|
|
28
|
+
* reader's own figures, rather than reporting a 1% win as a success.
|
|
29
|
+
*
|
|
30
|
+
* ## Every figure here is arithmetic on tokens that were billed
|
|
31
|
+
*
|
|
32
|
+
* Nothing is modelled and nothing is extrapolated. A route lever is the same token
|
|
33
|
+
* counts at another model's published rate. A batch lever is the same tokens at the
|
|
34
|
+
* provider's batch multiplier. There is no assumed traffic, no assumed prompt, no
|
|
35
|
+
* assumed anything — which is the whole reason this reads a usage log instead of a
|
|
36
|
+
* directory.
|
|
37
|
+
*
|
|
38
|
+
* ## What it refuses to do
|
|
39
|
+
*
|
|
40
|
+
* **It never says a lever is safe to take.** Routing a workload to a cheaper model
|
|
41
|
+
* is a quality question that arithmetic cannot answer, and this module has never
|
|
42
|
+
* seen the prompt or a single answer. So a route carries its dollar figure *and*
|
|
43
|
+
* the command that measures whether it holds, and it is described as worth testing
|
|
44
|
+
* rather than worth doing. The same posture the `model-downgrade` advisory has
|
|
45
|
+
* always had, for the same reason.
|
|
46
|
+
*
|
|
47
|
+
* **It never says "per month".** A usage log covers whatever period somebody
|
|
48
|
+
* happened to record, and this module is not told which. Every figure is "on this
|
|
49
|
+
* bill" — over exactly the calls in the file. Multiplying an unknown period into a
|
|
50
|
+
* monthly headline is how a tool ends up quoting a saving four times the real one.
|
|
51
|
+
*
|
|
52
|
+
* **It never crosses a vendor.** A cheaper model at another provider is a
|
|
53
|
+
* migration, not a routing change, and pricing one as though it were a switch you
|
|
54
|
+
* could make on Tuesday is a saving nobody can take.
|
|
55
|
+
*/
|
|
56
|
+
/** What a lever is. */
|
|
57
|
+
export type LeverId =
|
|
58
|
+
/** Send these calls to a cheaper model of the same family. */
|
|
59
|
+
'route'
|
|
60
|
+
/** Send these calls through the Batch API. */
|
|
61
|
+
| 'batch';
|
|
62
|
+
/**
|
|
63
|
+
* Everything available on one label-and-model slice, and what it comes to.
|
|
64
|
+
*
|
|
65
|
+
* **Grouped by slice rather than listed as separate levers, because the levers
|
|
66
|
+
* are not additive and a list invites adding them.** The first version printed
|
|
67
|
+
* "route support-rag: $12.60" and "batch support-rag: $10.50" as two rows against
|
|
68
|
+
* a slice that had only spent $21.00 — a reader who added them got $23.10, a
|
|
69
|
+
* saving larger than the bill it came from. Impossible, and in the flattering
|
|
70
|
+
* direction.
|
|
71
|
+
*
|
|
72
|
+
* They do combine, just not by addition: batching a routed call saves half of the
|
|
73
|
+
* *cheaper* model's price, not half of the one you left. `combinedUsd` is that
|
|
74
|
+
* figure, computed rather than summed.
|
|
75
|
+
*/
|
|
76
|
+
export interface SliceLevers {
|
|
77
|
+
/** The label these calls carry, or `UNLABELLED`. */
|
|
78
|
+
label: string;
|
|
79
|
+
/** The model they go to now. */
|
|
80
|
+
model: string;
|
|
81
|
+
modelName: string;
|
|
82
|
+
/** Calls affected — the reader's own judgement of whether it is worth a day. */
|
|
83
|
+
calls: number;
|
|
84
|
+
/** What these exact calls cost. */
|
|
85
|
+
spentUsd: number;
|
|
86
|
+
/** A cheaper model one capability step down, if the catalogue has one. */
|
|
87
|
+
route: {
|
|
88
|
+
candidate: {
|
|
89
|
+
id: string;
|
|
90
|
+
displayName: string;
|
|
91
|
+
};
|
|
92
|
+
savingUsd: number;
|
|
93
|
+
} | null;
|
|
94
|
+
/** The Batch API, where the provider sells one. */
|
|
95
|
+
batch: {
|
|
96
|
+
savingUsd: number;
|
|
97
|
+
} | null;
|
|
98
|
+
/**
|
|
99
|
+
* Both together, **computed and never summed**. Equal to the single available
|
|
100
|
+
* lever when only one is.
|
|
101
|
+
*/
|
|
102
|
+
combinedUsd: number;
|
|
103
|
+
/** `combinedUsd` as a fraction of the whole bill in the log, not of this slice. */
|
|
104
|
+
shareOfBill: number;
|
|
105
|
+
}
|
|
106
|
+
export interface BillLevers {
|
|
107
|
+
/** Ranked by what the whole slice could save, largest first. */
|
|
108
|
+
slices: SliceLevers[];
|
|
109
|
+
/**
|
|
110
|
+
* The most that shortening prompt text could ever be worth on this bill.
|
|
111
|
+
*
|
|
112
|
+
* Everything that is not output: plain input, cache reads, cache writes. It is a
|
|
113
|
+
* **ceiling and not an estimate** — deliberately generous, because it counts
|
|
114
|
+
* retrieved context, conversation history and tool results, none of which live in
|
|
115
|
+
* a prompt file and none of which a rules pass can touch. The real figure is
|
|
116
|
+
* below it, usually far below.
|
|
117
|
+
*
|
|
118
|
+
* It is here so the levers above have something to be compared against. A tool
|
|
119
|
+
* that reports a 1% win without saying 1% of what is not being useful.
|
|
120
|
+
*/
|
|
121
|
+
promptCeilingUsd: number;
|
|
122
|
+
promptCeilingShare: number;
|
|
123
|
+
/** The bill the shares are taken against. */
|
|
124
|
+
totalUsd: number;
|
|
125
|
+
}
|
|
126
|
+
export interface BillLeverOptions {
|
|
127
|
+
catalogue: PricingCatalogue;
|
|
128
|
+
/** Date the prices are read at, so a promotional rate resolves the same way. */
|
|
129
|
+
on?: Date;
|
|
130
|
+
/**
|
|
131
|
+
* Slices worth less than this share of the bill are dropped.
|
|
132
|
+
*
|
|
133
|
+
* Not a judgement about small money — a judgement about attention. Thirty rows
|
|
134
|
+
* worth a tenth of a percent each bury the two worth twenty, and a report nobody
|
|
135
|
+
* finishes reading is a report that changed nothing. Default 1%.
|
|
136
|
+
*/
|
|
137
|
+
minShare?: number;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Prices the levers that are not the prompt, from a profile of real calls.
|
|
141
|
+
*
|
|
142
|
+
* Returns them ranked by money, with the ceiling on prompt shortening beside them
|
|
143
|
+
* so the comparison is unavoidable. Empty when nothing clears `minShare`, which is
|
|
144
|
+
* a legitimate answer: a bill already on the cheapest model of its family, with no
|
|
145
|
+
* batch API to reach for, has no lever here, and saying so is more useful than
|
|
146
|
+
* manufacturing one.
|
|
147
|
+
*/
|
|
148
|
+
export declare function billLevers(report: UsageProfileReport, options: BillLeverOptions): BillLevers;
|
|
149
|
+
/** Named so a report can say "unlabelled" in the reader's language. */
|
|
150
|
+
export { UNLABELLED };
|
|
151
|
+
//# sourceMappingURL=levers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"levers.d.ts","sourceRoot":"","sources":["../src/levers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAkB,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,uBAAuB;AACvB,MAAM,MAAM,OAAO;AACjB,8DAA8D;AAC5D,OAAO;AACT,8CAA8C;GAC5C,OAAO,CAAC;AAEZ;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,WAAW;IAC1B,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,KAAK,EAAE;QAAE,SAAS,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACpF,mDAAmD;IACnD,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACpC;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,gEAAgE;IAChE,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;CAClB;AA8FD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,gBAAgB,CAAC;IAC5B,gFAAgF;IAChF,EAAE,CAAC,EAAE,IAAI,CAAC;IACV;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,kBAAkB,EAC1B,OAAO,EAAE,gBAAgB,GACxB,UAAU,CAqFZ;AAED,uEAAuE;AACvE,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
package/dist/levers.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
2
|
+
import { UNLABELLED } from './usage.js';
|
|
3
|
+
/**
|
|
4
|
+
* Weakest first. A step *down* this ladder is what a route offers.
|
|
5
|
+
*
|
|
6
|
+
* `unknown` is absent on purpose rather than placed at one end: a model whose
|
|
7
|
+
* capability nobody recorded cannot be ranked against one whose capability is
|
|
8
|
+
* known, and guessing puts a real workload on a model chosen by a default value.
|
|
9
|
+
*/
|
|
10
|
+
const CAPABILITY_LADDER = ['small', 'mid', 'large', 'frontier'];
|
|
11
|
+
/**
|
|
12
|
+
* The next step down, or `null` at the bottom and for `unknown`.
|
|
13
|
+
*
|
|
14
|
+
* One step, not the cheapest available. Frontier to small is an 80% saving and a
|
|
15
|
+
* different product, and offering it as the headline would be the arithmetic
|
|
16
|
+
* leading the advice — exactly the failure this file is written against. The
|
|
17
|
+
* reader who wants the bigger jump can ask for it once the first one holds.
|
|
18
|
+
*/
|
|
19
|
+
function stepDown(capability) {
|
|
20
|
+
const at = CAPABILITY_LADDER.indexOf(capability);
|
|
21
|
+
return at <= 0 ? null : CAPABILITY_LADDER[at - 1];
|
|
22
|
+
}
|
|
23
|
+
/** What a breakdown's tokens would cost at a model's rates, split so batch can apply. */
|
|
24
|
+
function repriceAt(breakdown, model, on) {
|
|
25
|
+
const { inputPerMTok, outputPerMTok } = effectivePricing(model, on);
|
|
26
|
+
const rates = multipliersFor(model);
|
|
27
|
+
const per = (tokens, rate) => (tokens / 1_000_000) * rate;
|
|
28
|
+
const inputUsd = per(breakdown.inputTokens, inputPerMTok);
|
|
29
|
+
const outputUsd = per(breakdown.outputTokens, outputPerMTok);
|
|
30
|
+
/**
|
|
31
|
+
* Cache writes at the 5-minute rate. The breakdown does not carry the recorded
|
|
32
|
+
* TTL split per class, so one has to be chosen — and the same choice sits on
|
|
33
|
+
* both sides of every subtraction here, so it cancels out of the saving. Worth
|
|
34
|
+
* stating rather than papering over: it would not cancel if the two models had
|
|
35
|
+
* different write multipliers, which is why a route never crosses a vendor.
|
|
36
|
+
*/
|
|
37
|
+
const cacheUsd = per(breakdown.cacheReadTokens, inputPerMTok * rates.cacheRead) +
|
|
38
|
+
per(breakdown.cacheWriteTokens, inputPerMTok * rates.cacheWrite5m);
|
|
39
|
+
return { inputUsd, outputUsd, cacheUsd, totalUsd: inputUsd + outputUsd + cacheUsd };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The cheapest recommendable model one capability step below, same provider.
|
|
43
|
+
*
|
|
44
|
+
* Same provider because switching vendor is a migration rather than a routing
|
|
45
|
+
* change, and the context window has to hold what these calls already sent — a
|
|
46
|
+
* cheaper model that cannot fit the prompt is not cheaper, it is broken.
|
|
47
|
+
*/
|
|
48
|
+
function candidateFor(model, breakdown, catalogue, on) {
|
|
49
|
+
const target = stepDown(model.capability);
|
|
50
|
+
if (target === null)
|
|
51
|
+
return null;
|
|
52
|
+
/**
|
|
53
|
+
* The largest single call cannot be recovered from a total, so this uses the
|
|
54
|
+
* **average** input per call and refuses any candidate that could not hold it.
|
|
55
|
+
* An average understates the peak, so this is the permissive direction — stated
|
|
56
|
+
* rather than hidden, because the reader will check the window properly when
|
|
57
|
+
* they run the evaluation this points them at.
|
|
58
|
+
*/
|
|
59
|
+
const avgInput = breakdown.calls === 0
|
|
60
|
+
? 0
|
|
61
|
+
: (breakdown.inputTokens + breakdown.cacheReadTokens + breakdown.cacheWriteTokens) /
|
|
62
|
+
breakdown.calls;
|
|
63
|
+
const candidates = catalogue.models.filter((m) => m.id !== model.id &&
|
|
64
|
+
m.capability === target &&
|
|
65
|
+
m.provider === model.provider &&
|
|
66
|
+
m.recommendable !== false &&
|
|
67
|
+
m.contextWindow >= avgInput);
|
|
68
|
+
if (candidates.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
return candidates.reduce((best, m) => repriceAt(breakdown, m, on).totalUsd < repriceAt(breakdown, best, on).totalUsd ? m : best);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Prices the levers that are not the prompt, from a profile of real calls.
|
|
74
|
+
*
|
|
75
|
+
* Returns them ranked by money, with the ceiling on prompt shortening beside them
|
|
76
|
+
* so the comparison is unavoidable. Empty when nothing clears `minShare`, which is
|
|
77
|
+
* a legitimate answer: a bill already on the cheapest model of its family, with no
|
|
78
|
+
* batch API to reach for, has no lever here, and saying so is more useful than
|
|
79
|
+
* manufacturing one.
|
|
80
|
+
*/
|
|
81
|
+
export function billLevers(report, options) {
|
|
82
|
+
const { catalogue, on = new Date(), minShare = 0.01 } = options;
|
|
83
|
+
const totalUsd = report.total.totalUsd;
|
|
84
|
+
const promptCeilingUsd = report.total.inputUsd + report.total.cacheReadUsd + report.total.cacheWriteUsd;
|
|
85
|
+
if (totalUsd <= 0) {
|
|
86
|
+
return { slices: [], promptCeilingUsd, promptCeilingShare: 0, totalUsd };
|
|
87
|
+
}
|
|
88
|
+
const slices = [];
|
|
89
|
+
for (const { label, model: modelId, breakdown } of report.byLabelAndModel) {
|
|
90
|
+
const model = catalogue.byId.get(modelId);
|
|
91
|
+
// An unpriced model never reaches this list with dollars on it, and a lever
|
|
92
|
+
// computed from a zero bill is a saving invented out of nothing.
|
|
93
|
+
if (!model || breakdown.totalUsd <= 0)
|
|
94
|
+
continue;
|
|
95
|
+
const candidate = candidateFor(model, breakdown, catalogue, on);
|
|
96
|
+
const routed = candidate ? repriceAt(breakdown, candidate, on) : null;
|
|
97
|
+
const route = candidate && routed && breakdown.totalUsd - routed.totalUsd > 0
|
|
98
|
+
? {
|
|
99
|
+
candidate: { id: candidate.id, displayName: candidate.displayName },
|
|
100
|
+
savingUsd: breakdown.totalUsd - routed.totalUsd,
|
|
101
|
+
}
|
|
102
|
+
: null;
|
|
103
|
+
/**
|
|
104
|
+
* `null` means the provider has no batch API, which is different from an
|
|
105
|
+
* unstated one — offering a discount nobody sells is worse than staying quiet.
|
|
106
|
+
*
|
|
107
|
+
* Applied to input and output only. The published discount covers those two
|
|
108
|
+
* lines; whether it also reaches cache reads and writes is not something this
|
|
109
|
+
* catalogue records, so they stay at full price. That understates the saving,
|
|
110
|
+
* which is the direction to be wrong in.
|
|
111
|
+
*/
|
|
112
|
+
const batchRate = multipliersFor(model).batch;
|
|
113
|
+
const batchable = batchRate !== null && batchRate < 1;
|
|
114
|
+
const batch = batchable
|
|
115
|
+
? { savingUsd: (breakdown.inputUsd + breakdown.outputUsd) * (1 - batchRate) }
|
|
116
|
+
: null;
|
|
117
|
+
/**
|
|
118
|
+
* Both together — **computed, never summed.**
|
|
119
|
+
*
|
|
120
|
+
* Batching a routed call discounts the cheaper model's price, not the one you
|
|
121
|
+
* left behind. Adding the two figures produced a saving larger than the slice
|
|
122
|
+
* had ever cost: $12.60 and $10.50 against $21.00 spent.
|
|
123
|
+
*/
|
|
124
|
+
const afterBoth = routed ?? {
|
|
125
|
+
inputUsd: breakdown.inputUsd,
|
|
126
|
+
outputUsd: breakdown.outputUsd,
|
|
127
|
+
cacheUsd: breakdown.cacheReadUsd + breakdown.cacheWriteUsd,
|
|
128
|
+
totalUsd: breakdown.totalUsd,
|
|
129
|
+
};
|
|
130
|
+
const combinedCost = batchable
|
|
131
|
+
? afterBoth.totalUsd - (afterBoth.inputUsd + afterBoth.outputUsd) * (1 - batchRate)
|
|
132
|
+
: afterBoth.totalUsd;
|
|
133
|
+
const combinedUsd = breakdown.totalUsd - combinedCost;
|
|
134
|
+
if (combinedUsd <= 0)
|
|
135
|
+
continue;
|
|
136
|
+
const shareOfBill = combinedUsd / totalUsd;
|
|
137
|
+
if (shareOfBill < minShare)
|
|
138
|
+
continue;
|
|
139
|
+
slices.push({
|
|
140
|
+
label,
|
|
141
|
+
model: modelId,
|
|
142
|
+
modelName: model.displayName,
|
|
143
|
+
calls: breakdown.calls,
|
|
144
|
+
spentUsd: breakdown.totalUsd,
|
|
145
|
+
route,
|
|
146
|
+
batch,
|
|
147
|
+
combinedUsd,
|
|
148
|
+
shareOfBill,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
slices: slices.sort((a, b) => b.combinedUsd - a.combinedUsd),
|
|
153
|
+
promptCeilingUsd,
|
|
154
|
+
promptCeilingShare: promptCeilingUsd / totalUsd,
|
|
155
|
+
totalUsd,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** Named so a report can say "unlabelled" in the reader's language. */
|
|
159
|
+
export { UNLABELLED };
|
|
160
|
+
//# sourceMappingURL=levers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"levers.js","sourceRoot":"","sources":["../src/levers.ts"],"names":[],"mappings":"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"}
|
package/dist/node.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
export { loadConfig } from './config.js';
|
|
18
18
|
export type { LoadConfigOptions, LoadedConfig } from './config.js';
|
|
19
19
|
export { CONFIG_FILENAME, CONFIG_KEYS, CONFIG_USAGE_KEYS, ConfigError, DEFAULT_EXTENSIONS, MAX_CONFIG_BYTES, MAX_CONFIG_SEARCH_DEPTH, budgetFor, parseConfig, validateConfigModel, } from './config-schema.js';
|
|
20
|
-
export type { ResolvedBudget, TrazumConfig } from './config-schema.js';
|
|
20
|
+
export type { ResolvedBudget, SpendConfig, TrazumConfig } from './config-schema.js';
|
|
21
21
|
export { MAX_PRICING_BYTES, PricingOverlayError, applyPricingOverlay, catalogueFromOverlay, parsePricingOverlay, } from './pricing-overlay.js';
|
|
22
22
|
export type { PricingOverlay } from './pricing-overlay.js';
|
|
23
23
|
export { openrouterOverlay } from './openrouter.js';
|
package/dist/node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAInE,OAAO,EACL,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,uBAAuB,EACvB,SAAS,EACT,WAAW,EACX,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAInE,OAAO,EACL,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,uBAAuB,EACvB,SAAS,EACT,WAAW,EACX,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIpF,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAI3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGxD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { PricingCatalogue } from './pricing.js';
|
|
2
|
+
import type { UsageRecord } from './usage.js';
|
|
3
|
+
/**
|
|
4
|
+
* Where the output spend concentrates.
|
|
5
|
+
*
|
|
6
|
+
* ## The biggest line, and nothing said anything actionable about it
|
|
7
|
+
*
|
|
8
|
+
* Output is over half of many real bills — **87%** on the support prompt this
|
|
9
|
+
* repository measures itself against. `profile` could say that much and then
|
|
10
|
+
* stopped, because the advice that follows from "output dominates" is about
|
|
11
|
+
* answers rather than prompts, and the rules engine has nothing to offer there.
|
|
12
|
+
*
|
|
13
|
+
* But a total hides the shape, and the shape is the actionable part. Two bills
|
|
14
|
+
* with identical output spend want opposite responses:
|
|
15
|
+
*
|
|
16
|
+
* - **A tail.** Six per cent of calls hold half the output spend. Those calls are
|
|
17
|
+
* doing something the other ninety-four are not — a different path through the
|
|
18
|
+
* prompt, a runaway with no `max_tokens`, a retrieval that returned a book. They
|
|
19
|
+
* are a morning's work and they are worth finding.
|
|
20
|
+
* - **Flat.** Forty-five per cent of calls hold half of it, which is what "evenly
|
|
21
|
+
* spread" looks like. There is no tail to hunt; the answer length is inherent to
|
|
22
|
+
* the task, and the only lever is asking every answer to be shorter.
|
|
23
|
+
*
|
|
24
|
+
* ## The split is derived, not chosen
|
|
25
|
+
*
|
|
26
|
+
* The figure reported is **the smallest group of calls that holds at least half the
|
|
27
|
+
* output spend**. Half is the point that divides the spend in two — a median over
|
|
28
|
+
* money rather than a threshold somebody picked — and the group is found by walking
|
|
29
|
+
* the distribution down from the longest answers until half the spend is covered.
|
|
30
|
+
*
|
|
31
|
+
* "At least half" is meant literally. The walk stops on a bucket boundary, so the
|
|
32
|
+
* group it names is a whole number of buckets and can overshoot; saying "half"
|
|
33
|
+
* flat would be claiming a precision the histogram does not have.
|
|
34
|
+
*
|
|
35
|
+
* ## Bounded memory, exact statement
|
|
36
|
+
*
|
|
37
|
+
* The counts live in fixed buckets rather than a list of every call, because a
|
|
38
|
+
* usage log is measured in megabytes. Every call inside an included bucket is at or
|
|
39
|
+
* above that bucket's lower edge, so **"calls producing more than N tokens" is
|
|
40
|
+
* exact** for the N this reports — it is only ever a bucket edge.
|
|
41
|
+
*/
|
|
42
|
+
/** How the output spend of one label-and-model slice is distributed. */
|
|
43
|
+
export interface OutputShape {
|
|
44
|
+
label: string;
|
|
45
|
+
model: string;
|
|
46
|
+
modelName: string;
|
|
47
|
+
calls: number;
|
|
48
|
+
outputTokens: number;
|
|
49
|
+
outputUsd: number;
|
|
50
|
+
/** The bucket edge the heaviest group sits above. Always a bucket boundary. */
|
|
51
|
+
aboveTokens: number;
|
|
52
|
+
/** How many calls are in that group. */
|
|
53
|
+
heavyCalls: number;
|
|
54
|
+
/** Their share of the calls in this slice. */
|
|
55
|
+
heavyCallShare: number;
|
|
56
|
+
/** Their share of this slice's output spend — at least a half, by construction. */
|
|
57
|
+
heavySpendShare: number;
|
|
58
|
+
/** This slice's output spend as a fraction of the whole bill. */
|
|
59
|
+
shareOfBill: number;
|
|
60
|
+
/**
|
|
61
|
+
* The bucket ceiling that at least half the measured answers fit within.
|
|
62
|
+
*
|
|
63
|
+
* A ceiling by construction, never an interpolation: the histogram knows
|
|
64
|
+
* which bucket the median call landed in, and the honest sentence is "half
|
|
65
|
+
* the answers fit within N tokens" where N is that bucket's upper edge.
|
|
66
|
+
* `null` only when the covering bucket is the open-ended last one, which has
|
|
67
|
+
* no ceiling to name.
|
|
68
|
+
*/
|
|
69
|
+
medianWithinTokens: number | null;
|
|
70
|
+
/**
|
|
71
|
+
* The same ceiling for 95% of the measured answers — the number somebody
|
|
72
|
+
* setting `max_tokens` actually wants. Measured on these calls, promised for
|
|
73
|
+
* nothing.
|
|
74
|
+
*/
|
|
75
|
+
p95WithinTokens: number | null;
|
|
76
|
+
}
|
|
77
|
+
export interface OutputShapeOptions {
|
|
78
|
+
catalogue: PricingCatalogue;
|
|
79
|
+
on?: Date;
|
|
80
|
+
/** Slices whose output is below this share of the bill are dropped. Default 5%. */
|
|
81
|
+
minShare?: number;
|
|
82
|
+
}
|
|
83
|
+
export interface OutputShapeTracker {
|
|
84
|
+
add(record: UsageRecord): void;
|
|
85
|
+
finish(totalUsd: number): OutputShape[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* An accumulator, fed in the pass a profile already makes.
|
|
89
|
+
*
|
|
90
|
+
* What it holds is bounded by the number of slices times the number of buckets any
|
|
91
|
+
* of them actually touches, not by the size of the log.
|
|
92
|
+
*/
|
|
93
|
+
export declare function createOutputShapeTracker(options: OutputShapeOptions): OutputShapeTracker;
|
|
94
|
+
/** The same measurement over a list of records, for a caller holding one. */
|
|
95
|
+
export declare function outputShapes(records: readonly UsageRecord[], totalUsd: number, options: OutputShapeOptions): OutputShape[];
|
|
96
|
+
//# sourceMappingURL=output-shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-shape.d.ts","sourceRoot":"","sources":["../src/output-shape.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,WAAW,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB,mFAAmF;IACnF,eAAe,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC;;;;OAIG;IACH,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,EAAE,CAAC,EAAE,IAAI,CAAC;IACV,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA2DD,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC;CACzC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,kBAAkB,GAAG,kBAAkB,CAsFxF;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAC1B,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,kBAAkB,GAC1B,WAAW,EAAE,CAIf"}
|