@trazum/core 1.35.0 → 1.37.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 +23 -0
- package/dist/config-schema.d.ts +18 -2
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +43 -1
- package/dist/config-schema.js.map +1 -1
- package/dist/fleet.d.ts +107 -0
- package/dist/fleet.d.ts.map +1 -0
- package/dist/fleet.js +138 -0
- package/dist/fleet.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/measured-profile.d.ts +126 -0
- package/dist/measured-profile.d.ts.map +1 -0
- package/dist/measured-profile.js +114 -0
- package/dist/measured-profile.js.map +1 -0
- package/package.json +1 -1
- package/src/config-schema.ts +58 -1
- package/src/fleet.ts +212 -0
- package/src/index.ts +4 -0
- package/src/measured-profile.ts +198 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usage profile `optimize` multiplies by, taken from a log instead of
|
|
3
|
+
* from somebody's typing.
|
|
4
|
+
*
|
|
5
|
+
* `optimize` prices a prompt change as `token delta × callsPerMonth`, with
|
|
6
|
+
* `avgOutputTokens` and `cacheHitRate` shaping the rest. Those three numbers
|
|
7
|
+
* are typed into a config file by a human who is guessing, and the two values
|
|
8
|
+
* most often typed are `1000` and whatever the README's example used. A usage
|
|
9
|
+
* log sitting in the same repository knows all three exactly.
|
|
10
|
+
*
|
|
11
|
+
* **What this module does not do is turn an estimate into a fact.** The token
|
|
12
|
+
* delta stays an estimate with its ±10% band; what stops being a guess is
|
|
13
|
+
* everything it is multiplied by. That distinction has to survive into the
|
|
14
|
+
* rendering, which is why `MeasuredUsage` carries the provenance of each
|
|
15
|
+
* figure rather than only the figures.
|
|
16
|
+
*
|
|
17
|
+
* The hardest decision here is the call count. A month's saving from a log
|
|
18
|
+
* covering three days is a forecast wearing arithmetic's clothes, and this
|
|
19
|
+
* repository refuses those. So the scaling has a floor, it is stated whenever
|
|
20
|
+
* it happens, and under the floor the tool reports the period it measured and
|
|
21
|
+
* declines the multiplication rather than performing it quietly.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { UNLABELLED } from './usage.js';
|
|
25
|
+
import type { UsageProfileReport } from './usage.js';
|
|
26
|
+
import type { UsageProfile } from './types.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The shortest span that may be scaled to a month, in days.
|
|
30
|
+
*
|
|
31
|
+
* A full week, because the week is the cycle traffic actually has: weekdays
|
|
32
|
+
* against weekends is the one periodicity nearly every workload shows, and a
|
|
33
|
+
* span shorter than one cycle scaled up multiplies whichever part of the
|
|
34
|
+
* cycle it happened to catch. Three weekdays scaled to a month is not a
|
|
35
|
+
* monthly figure, it is a Tuesday with a multiplier.
|
|
36
|
+
*
|
|
37
|
+
* Above the floor this is still a *rate*, not a prediction: "at the rate this
|
|
38
|
+
* log measured" is arithmetic about the past, and every rendering says so.
|
|
39
|
+
*/
|
|
40
|
+
export const MIN_SCALE_DAYS = 7;
|
|
41
|
+
|
|
42
|
+
/** Days in the month this scales to. Stated rather than hidden in a constant. */
|
|
43
|
+
export const SCALE_TO_DAYS = 30;
|
|
44
|
+
|
|
45
|
+
export interface MeasuredUsage {
|
|
46
|
+
/** Ready to hand to `optimize`. Every field below is measured, not typed. */
|
|
47
|
+
profile: UsageProfile;
|
|
48
|
+
/** Calls the log actually recorded for this slice. Never scaled. */
|
|
49
|
+
calls: number;
|
|
50
|
+
/** What those calls actually cost. The measured half of any comparison. */
|
|
51
|
+
spentUsd: number;
|
|
52
|
+
/**
|
|
53
|
+
* The period the calls fall in, in days, or null when no call carried a
|
|
54
|
+
* clock. Null is why `scaled` can be null with calls above the floor.
|
|
55
|
+
*/
|
|
56
|
+
spanDays: number | null;
|
|
57
|
+
/**
|
|
58
|
+
* How `callsPerMonth` was reached, or **null when it was not scaled at
|
|
59
|
+
* all** — in which case `profile.callsPerMonth` is the raw measured count
|
|
60
|
+
* over whatever period the log covers, and the rendering must say so
|
|
61
|
+
* rather than printing it under a "per month" heading.
|
|
62
|
+
*/
|
|
63
|
+
scaled: { fromDays: number; factor: number } | null;
|
|
64
|
+
/**
|
|
65
|
+
* The share of input tokens served from cache, 0–1.
|
|
66
|
+
*
|
|
67
|
+
* `UsageProfile.cacheHitRate` is documented as the fraction of *calls* that
|
|
68
|
+
* reuse the prefix; what a log can measure is the fraction of input
|
|
69
|
+
* *tokens* that were cache reads. Those are the same number only when every
|
|
70
|
+
* call is the same size. It is handed over as the better of two
|
|
71
|
+
* approximations — a measured token share beats a typed call share — and
|
|
72
|
+
* named honestly here so no rendering can call it something it is not.
|
|
73
|
+
*/
|
|
74
|
+
cacheReadShare: number;
|
|
75
|
+
/**
|
|
76
|
+
* The model carrying the most spend in this slice, and how many models the
|
|
77
|
+
* slice used at all. Above one, the single model handed to `optimize` is a
|
|
78
|
+
* simplification and the rendering says which share it covers.
|
|
79
|
+
*/
|
|
80
|
+
models: { chosen: string; count: number; chosenShareOfSpend: number };
|
|
81
|
+
/** True when no call in the slice recorded any output tokens. */
|
|
82
|
+
outputUnmeasured: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Derives the usage profile for one label from a measured report.
|
|
87
|
+
*
|
|
88
|
+
* Returns `null` when the label carries no priced calls — a slice with no
|
|
89
|
+
* measured traffic has nothing to hand over, and inventing a zero-call
|
|
90
|
+
* profile would produce a $0 saving that reads as "this change is worthless"
|
|
91
|
+
* rather than "nothing here was measured".
|
|
92
|
+
*/
|
|
93
|
+
export function measuredUsage(
|
|
94
|
+
report: UsageProfileReport,
|
|
95
|
+
label: string,
|
|
96
|
+
options: { batchEligible?: boolean } = {},
|
|
97
|
+
): MeasuredUsage | null {
|
|
98
|
+
const slices = report.byLabelAndModel.filter((row) => row.label === label);
|
|
99
|
+
if (slices.length === 0) return null;
|
|
100
|
+
|
|
101
|
+
const calls = slices.reduce((sum, row) => sum + row.breakdown.calls, 0);
|
|
102
|
+
if (calls === 0) return null;
|
|
103
|
+
|
|
104
|
+
const spentUsd = slices.reduce((sum, row) => sum + row.breakdown.totalUsd, 0);
|
|
105
|
+
const outputTokens = slices.reduce((sum, row) => sum + row.breakdown.outputTokens, 0);
|
|
106
|
+
const inputTokens = slices.reduce((sum, row) => sum + row.breakdown.inputTokens, 0);
|
|
107
|
+
const cacheReadTokens = slices.reduce((sum, row) => sum + row.breakdown.cacheReadTokens, 0);
|
|
108
|
+
|
|
109
|
+
// The model carrying the most spend. Ties break on the larger call count, so
|
|
110
|
+
// the answer is stable rather than dependent on map ordering.
|
|
111
|
+
const ranked = [...slices].sort(
|
|
112
|
+
(a, b) => b.breakdown.totalUsd - a.breakdown.totalUsd || b.breakdown.calls - a.breakdown.calls,
|
|
113
|
+
);
|
|
114
|
+
const chosen = ranked[0]!;
|
|
115
|
+
const chosenShareOfSpend = spentUsd > 0 ? chosen.breakdown.totalUsd / spentUsd : 0;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The span of *this slice*, not of the whole log. A label active for three
|
|
119
|
+
* days of a thirty-day log has a three-day rate, and using the log's span
|
|
120
|
+
* would divide its calls across weeks it never ran in.
|
|
121
|
+
*/
|
|
122
|
+
const spanDays =
|
|
123
|
+
report.span === null ? null : (report.span.toMs - report.span.fromMs) / 86_400_000;
|
|
124
|
+
|
|
125
|
+
const scaled =
|
|
126
|
+
spanDays !== null && spanDays >= MIN_SCALE_DAYS
|
|
127
|
+
? { fromDays: spanDays, factor: SCALE_TO_DAYS / spanDays }
|
|
128
|
+
: null;
|
|
129
|
+
|
|
130
|
+
const readShare =
|
|
131
|
+
inputTokens + cacheReadTokens > 0 ? cacheReadTokens / (inputTokens + cacheReadTokens) : 0;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
profile: {
|
|
135
|
+
model: chosen.model,
|
|
136
|
+
// Scaled when the span earns it, the raw measured count otherwise. The
|
|
137
|
+
// caller must read `scaled` before printing this under any heading that
|
|
138
|
+
// says "month".
|
|
139
|
+
callsPerMonth: scaled === null ? calls : Math.round(calls * scaled.factor),
|
|
140
|
+
avgOutputTokens: Math.round(outputTokens / calls),
|
|
141
|
+
cacheHitRate: readShare,
|
|
142
|
+
batchEligible: options.batchEligible ?? false,
|
|
143
|
+
},
|
|
144
|
+
calls,
|
|
145
|
+
spentUsd,
|
|
146
|
+
spanDays,
|
|
147
|
+
scaled,
|
|
148
|
+
cacheReadShare: readShare,
|
|
149
|
+
models: { chosen: chosen.model, count: slices.length, chosenShareOfSpend },
|
|
150
|
+
outputUnmeasured: outputTokens === 0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Every label the report priced, with the prompt file the config maps to it —
|
|
156
|
+
* and, deliberately, both kinds of mismatch.
|
|
157
|
+
*
|
|
158
|
+
* The two failures this surfaces are the ones a person cannot see from either
|
|
159
|
+
* side alone: a prompt file mapped to a label that no longer appears in the
|
|
160
|
+
* log (renamed, retired, or a typo that has been silently doing nothing), and
|
|
161
|
+
* a label carrying real money with no prompt file mapped at all (the workload
|
|
162
|
+
* nobody can optimise because nobody said where it lives).
|
|
163
|
+
*/
|
|
164
|
+
export interface LabelCoverage {
|
|
165
|
+
/** Labels with both traffic and a mapped prompt file. */
|
|
166
|
+
joined: { label: string; promptPath: string; spentUsd: number }[];
|
|
167
|
+
/** Mapped prompt files whose label has no priced traffic in this log. */
|
|
168
|
+
mappedWithoutTraffic: { label: string; promptPath: string }[];
|
|
169
|
+
/** Labels with priced traffic and no prompt file mapped, dearest first. */
|
|
170
|
+
trafficWithoutPrompt: { label: string; spentUsd: number }[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function labelCoverage(
|
|
174
|
+
report: UsageProfileReport,
|
|
175
|
+
labels: Record<string, string>,
|
|
176
|
+
): LabelCoverage {
|
|
177
|
+
const spendByLabel = new Map(report.byLabel.map((row) => [row.label, row.breakdown.totalUsd]));
|
|
178
|
+
|
|
179
|
+
const joined: LabelCoverage['joined'] = [];
|
|
180
|
+
const mappedWithoutTraffic: LabelCoverage['mappedWithoutTraffic'] = [];
|
|
181
|
+
for (const [label, promptPath] of Object.entries(labels)) {
|
|
182
|
+
const spentUsd = spendByLabel.get(label);
|
|
183
|
+
if (spentUsd === undefined) mappedWithoutTraffic.push({ label, promptPath });
|
|
184
|
+
else joined.push({ label, promptPath, spentUsd });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const trafficWithoutPrompt = report.byLabel
|
|
188
|
+
// The unlabelled bucket is not a workload somebody forgot to map; it is
|
|
189
|
+
// calls that carry no label at all, which `fieldCoverage` already reports.
|
|
190
|
+
.filter((row) => row.label !== UNLABELLED && labels[row.label] === undefined)
|
|
191
|
+
.map((row) => ({ label: row.label, spentUsd: row.breakdown.totalUsd }))
|
|
192
|
+
.sort((a, b) => b.spentUsd - a.spentUsd);
|
|
193
|
+
|
|
194
|
+
joined.sort((a, b) => b.spentUsd - a.spentUsd);
|
|
195
|
+
mappedWithoutTraffic.sort((a, b) => a.label.localeCompare(b.label));
|
|
196
|
+
|
|
197
|
+
return { joined, mappedWithoutTraffic, trafficWithoutPrompt };
|
|
198
|
+
}
|