@trazum/core 1.36.0 → 1.38.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 +25 -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/plan.d.ts +125 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +125 -0
- package/dist/plan.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/plan.ts +207 -0
package/dist/plan.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Not a list of findings — a ranked, costed, non-additive plan of what to do.
|
|
3
|
+
*
|
|
4
|
+
* The report names findings; a person then decides what to do first by doing
|
|
5
|
+
* arithmetic in their head, and head-arithmetic on savings gets done by
|
|
6
|
+
* *adding* them — which the levers module has documented as wrong since it
|
|
7
|
+
* shipped ($12.60 plus $10.50 against a $21.00 slice). This module does the
|
|
8
|
+
* composition once, correctly, and attaches to every action the things the
|
|
9
|
+
* log cannot confirm, because a plan that hides its assumptions is advice
|
|
10
|
+
* pretending to be arithmetic.
|
|
11
|
+
*
|
|
12
|
+
* **Everything here is derived from figures the report already computed.**
|
|
13
|
+
* Route and batch come from `billLevers` (combined, never summed). The
|
|
14
|
+
* truncation action's stake is the retry bill `truncationRetries` measured.
|
|
15
|
+
* The cache action's stake is `cacheEconomics`' own delta. Nothing is
|
|
16
|
+
* invented, and each action carries how to check the part that is not
|
|
17
|
+
* arithmetic.
|
|
18
|
+
*
|
|
19
|
+
* **The total is stated honestly.** Actions on *different* slices add
|
|
20
|
+
* cleanly; the one composition that does not add — route and batch on the
|
|
21
|
+
* same slice — arrives already combined inside a single action, so the
|
|
22
|
+
* plan's total is a sum of non-overlapping figures by construction. Measured
|
|
23
|
+
* stakes (money already spent on retries, money already lost to caching) are
|
|
24
|
+
* totalled separately from projected savings: "what you would save" and
|
|
25
|
+
* "what you already paid" are different columns, and merging them makes a
|
|
26
|
+
* number that is neither.
|
|
27
|
+
*/
|
|
28
|
+
import type { UsageProfileReport } from './usage.js';
|
|
29
|
+
import type { BillLevers } from './levers.js';
|
|
30
|
+
export type PlanActionKind = 'route' | 'batch' | 'route+batch' | 'fix-truncation' | 'fix-caching';
|
|
31
|
+
/**
|
|
32
|
+
* What the log cannot confirm, as data rather than prose.
|
|
33
|
+
*
|
|
34
|
+
* Rendering lives with whoever renders — the CLI localizes these, and 1.39's
|
|
35
|
+
* verification can match them structurally. An English sentence baked in here
|
|
36
|
+
* would be a browser-safe module deciding the reader's language.
|
|
37
|
+
*/
|
|
38
|
+
export type PlanAssumption =
|
|
39
|
+
/** The cheaper model can actually do this work — quality, not arithmetic. */
|
|
40
|
+
{
|
|
41
|
+
kind: 'model-capability';
|
|
42
|
+
model: string;
|
|
43
|
+
}
|
|
44
|
+
/** These calls tolerate a batch window's latency. */
|
|
45
|
+
| {
|
|
46
|
+
kind: 'batch-window';
|
|
47
|
+
}
|
|
48
|
+
/** The truncation-retry pairing is real — the log sees shapes, not content. */
|
|
49
|
+
| {
|
|
50
|
+
kind: 'retry-pattern-real';
|
|
51
|
+
}
|
|
52
|
+
/** A max_tokens the answers fit inside removes the retry pair. */
|
|
53
|
+
| {
|
|
54
|
+
kind: 'max-tokens-fits';
|
|
55
|
+
}
|
|
56
|
+
/** The traffic pattern holds — a cache underwater on this log may pay on other traffic. */
|
|
57
|
+
| {
|
|
58
|
+
kind: 'traffic-pattern-holds';
|
|
59
|
+
};
|
|
60
|
+
export interface PlanAction {
|
|
61
|
+
kind: PlanActionKind;
|
|
62
|
+
/** The workload this acts on. `UNLABELLED` renders as the unlabelled bucket. */
|
|
63
|
+
label: string;
|
|
64
|
+
model: string;
|
|
65
|
+
/**
|
|
66
|
+
* Projected saving per the log's own period, for route/batch — or null for
|
|
67
|
+
* the measured-stake actions, whose money is in `stakeUsd` instead. Never
|
|
68
|
+
* both: a projection and a measurement in one field is a number that is
|
|
69
|
+
* neither.
|
|
70
|
+
*/
|
|
71
|
+
savingUsd: number | null;
|
|
72
|
+
/**
|
|
73
|
+
* Money already measured against this problem — the retry bill, the cache
|
|
74
|
+
* loss. Null for the projected actions.
|
|
75
|
+
*/
|
|
76
|
+
stakeUsd: number | null;
|
|
77
|
+
/** What the log cannot confirm. Every entry is a human's question to answer. */
|
|
78
|
+
assumes: PlanAssumption[];
|
|
79
|
+
/** How to check the assumption, when a Trazum command can. */
|
|
80
|
+
check: string | null;
|
|
81
|
+
/** What this action does, in one machine-stable keyword per detail. */
|
|
82
|
+
detail: {
|
|
83
|
+
/** Route target, when the action moves the calls. */
|
|
84
|
+
routeTo?: {
|
|
85
|
+
id: string;
|
|
86
|
+
displayName: string;
|
|
87
|
+
};
|
|
88
|
+
/** The measured pieces behind a stake. */
|
|
89
|
+
measured?: Record<string, number>;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export interface PlanDocument {
|
|
93
|
+
/** Same contract discipline as the profile JSON. */
|
|
94
|
+
schemaVersion: 1;
|
|
95
|
+
/** The period the plan's figures cover, or null when the log had no clock. */
|
|
96
|
+
span: {
|
|
97
|
+
fromMs: number;
|
|
98
|
+
toMs: number;
|
|
99
|
+
} | null;
|
|
100
|
+
/** The price table behind every dollar here. */
|
|
101
|
+
pricingLastReviewed: string;
|
|
102
|
+
/** Ranked: largest money first, projected or staked alike. */
|
|
103
|
+
actions: PlanAction[];
|
|
104
|
+
/**
|
|
105
|
+
* Projected savings summed — additive by construction, because same-slice
|
|
106
|
+
* compositions arrive pre-combined in one action.
|
|
107
|
+
*/
|
|
108
|
+
projectedSavingUsd: number;
|
|
109
|
+
/** Measured stakes summed: money already paid to problems this plan names. */
|
|
110
|
+
measuredStakeUsd: number;
|
|
111
|
+
/** The bill the plan was made against. */
|
|
112
|
+
totalUsd: number;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Builds the plan from a report and its levers.
|
|
116
|
+
*
|
|
117
|
+
* `pricingLastReviewed` is passed in rather than imported so the plan records
|
|
118
|
+
* the catalogue that actually priced it — an overlay's date when one was in
|
|
119
|
+
* effect, which 1.39's verification needs to tell "the prediction was wrong"
|
|
120
|
+
* from "the prices changed".
|
|
121
|
+
*/
|
|
122
|
+
export declare function buildPlan(report: UsageProfileReport, levers: BillLevers, pricingLastReviewed: string): PlanDocument;
|
|
123
|
+
/** Renders `UNLABELLED` for humans without leaking the sentinel. */
|
|
124
|
+
export declare function planLabelName(label: string, unlabelled: string): string;
|
|
125
|
+
//# sourceMappingURL=plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,aAAa,GAAG,gBAAgB,GAAG,aAAa,CAAC;AAElG;;;;;;GAMG;AACH,MAAM,MAAM,cAAc;AACxB,6EAA6E;AAC3E;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAC7C,qDAAqD;GACnD;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE;AAC1B,+EAA+E;GAC7E;IAAE,IAAI,EAAE,oBAAoB,CAAA;CAAE;AAChC,kEAAkE;GAChE;IAAE,IAAI,EAAE,iBAAiB,CAAA;CAAE;AAC7B,2FAA2F;GACzF;IAAE,IAAI,EAAE,uBAAuB,CAAA;CAAE,CAAC;AAEtC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,gFAAgF;IAChF,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,8DAA8D;IAC9D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,uEAAuE;IACvE,MAAM,EAAE;QACN,qDAAqD;QACrD,OAAO,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,MAAM,CAAA;SAAE,CAAC;QAC9C,0CAA0C;QAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACnC,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,oDAAoD;IACpD,aAAa,EAAE,CAAC,CAAC;IACjB,8EAA8E;IAC9E,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9C,gDAAgD;IAChD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB;;;OAGG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,UAAU,EAClB,mBAAmB,EAAE,MAAM,GAC1B,YAAY,CAsFd;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEvE"}
|
package/dist/plan.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Not a list of findings — a ranked, costed, non-additive plan of what to do.
|
|
3
|
+
*
|
|
4
|
+
* The report names findings; a person then decides what to do first by doing
|
|
5
|
+
* arithmetic in their head, and head-arithmetic on savings gets done by
|
|
6
|
+
* *adding* them — which the levers module has documented as wrong since it
|
|
7
|
+
* shipped ($12.60 plus $10.50 against a $21.00 slice). This module does the
|
|
8
|
+
* composition once, correctly, and attaches to every action the things the
|
|
9
|
+
* log cannot confirm, because a plan that hides its assumptions is advice
|
|
10
|
+
* pretending to be arithmetic.
|
|
11
|
+
*
|
|
12
|
+
* **Everything here is derived from figures the report already computed.**
|
|
13
|
+
* Route and batch come from `billLevers` (combined, never summed). The
|
|
14
|
+
* truncation action's stake is the retry bill `truncationRetries` measured.
|
|
15
|
+
* The cache action's stake is `cacheEconomics`' own delta. Nothing is
|
|
16
|
+
* invented, and each action carries how to check the part that is not
|
|
17
|
+
* arithmetic.
|
|
18
|
+
*
|
|
19
|
+
* **The total is stated honestly.** Actions on *different* slices add
|
|
20
|
+
* cleanly; the one composition that does not add — route and batch on the
|
|
21
|
+
* same slice — arrives already combined inside a single action, so the
|
|
22
|
+
* plan's total is a sum of non-overlapping figures by construction. Measured
|
|
23
|
+
* stakes (money already spent on retries, money already lost to caching) are
|
|
24
|
+
* totalled separately from projected savings: "what you would save" and
|
|
25
|
+
* "what you already paid" are different columns, and merging them makes a
|
|
26
|
+
* number that is neither.
|
|
27
|
+
*/
|
|
28
|
+
import { UNLABELLED, cacheEconomics } from './usage.js';
|
|
29
|
+
/**
|
|
30
|
+
* Builds the plan from a report and its levers.
|
|
31
|
+
*
|
|
32
|
+
* `pricingLastReviewed` is passed in rather than imported so the plan records
|
|
33
|
+
* the catalogue that actually priced it — an overlay's date when one was in
|
|
34
|
+
* effect, which 1.39's verification needs to tell "the prediction was wrong"
|
|
35
|
+
* from "the prices changed".
|
|
36
|
+
*/
|
|
37
|
+
export function buildPlan(report, levers, pricingLastReviewed) {
|
|
38
|
+
const actions = [];
|
|
39
|
+
for (const slice of levers.slices) {
|
|
40
|
+
const assumes = [];
|
|
41
|
+
let kind;
|
|
42
|
+
if (slice.route !== null && slice.batch !== null) {
|
|
43
|
+
kind = 'route+batch';
|
|
44
|
+
assumes.push({ kind: 'model-capability', model: slice.route.candidate.displayName });
|
|
45
|
+
assumes.push({ kind: 'batch-window' });
|
|
46
|
+
}
|
|
47
|
+
else if (slice.route !== null) {
|
|
48
|
+
kind = 'route';
|
|
49
|
+
assumes.push({ kind: 'model-capability', model: slice.route.candidate.displayName });
|
|
50
|
+
}
|
|
51
|
+
else if (slice.batch !== null) {
|
|
52
|
+
kind = 'batch';
|
|
53
|
+
assumes.push({ kind: 'batch-window' });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
actions.push({
|
|
59
|
+
kind,
|
|
60
|
+
label: slice.label,
|
|
61
|
+
model: slice.model,
|
|
62
|
+
savingUsd: slice.combinedUsd,
|
|
63
|
+
stakeUsd: null,
|
|
64
|
+
assumes,
|
|
65
|
+
check: slice.route !== null ? 'trazum route <log> --prompt-file <prompt> --cases <cases>' : null,
|
|
66
|
+
detail: slice.route !== null ? { routeTo: slice.route.candidate } : {},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
for (const row of report.truncationRetries) {
|
|
70
|
+
actions.push({
|
|
71
|
+
kind: 'fix-truncation',
|
|
72
|
+
label: row.label,
|
|
73
|
+
model: row.model,
|
|
74
|
+
savingUsd: null,
|
|
75
|
+
stakeUsd: row.wastedUsd + row.retryUsd,
|
|
76
|
+
assumes: [{ kind: 'retry-pattern-real' }, { kind: 'max-tokens-fits' }],
|
|
77
|
+
check: null,
|
|
78
|
+
detail: {
|
|
79
|
+
measured: {
|
|
80
|
+
wastedUsd: row.wastedUsd,
|
|
81
|
+
retryUsd: row.retryUsd,
|
|
82
|
+
retried: row.retried,
|
|
83
|
+
truncatedCalls: row.truncatedCalls,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
for (const slice of report.byLabelAndModel) {
|
|
89
|
+
const economics = cacheEconomics(slice.breakdown);
|
|
90
|
+
// Only a settled loss becomes an action: an unsettled verdict is a
|
|
91
|
+
// missing field, and "add the field" is the report's advice, not a plan's.
|
|
92
|
+
if (economics.verdict !== 'lost-money' || economics.worstCaseVerdict !== economics.verdict)
|
|
93
|
+
continue;
|
|
94
|
+
actions.push({
|
|
95
|
+
kind: 'fix-caching',
|
|
96
|
+
label: slice.label,
|
|
97
|
+
model: slice.model,
|
|
98
|
+
savingUsd: null,
|
|
99
|
+
stakeUsd: economics.deltaUsd,
|
|
100
|
+
assumes: [{ kind: 'traffic-pattern-holds' }],
|
|
101
|
+
check: null,
|
|
102
|
+
detail: {
|
|
103
|
+
measured: {
|
|
104
|
+
spentUsd: economics.spentUsd,
|
|
105
|
+
withoutCachingUsd: economics.withoutCachingUsd,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
actions.sort((a, b) => (b.savingUsd ?? b.stakeUsd ?? 0) - (a.savingUsd ?? a.stakeUsd ?? 0));
|
|
111
|
+
return {
|
|
112
|
+
schemaVersion: 1,
|
|
113
|
+
span: report.span === null ? null : { fromMs: report.span.fromMs, toMs: report.span.toMs },
|
|
114
|
+
pricingLastReviewed,
|
|
115
|
+
actions,
|
|
116
|
+
projectedSavingUsd: actions.reduce((sum, a) => sum + (a.savingUsd ?? 0), 0),
|
|
117
|
+
measuredStakeUsd: actions.reduce((sum, a) => sum + (a.stakeUsd ?? 0), 0),
|
|
118
|
+
totalUsd: report.total.totalUsd,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Renders `UNLABELLED` for humans without leaking the sentinel. */
|
|
122
|
+
export function planLabelName(label, unlabelled) {
|
|
123
|
+
return label === UNLABELLED ? unlabelled : label;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=plan.js.map
|
package/dist/plan.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../src/plan.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.
|
|
3
|
+
"version": "1.38.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",
|
package/src/config-schema.ts
CHANGED
|
@@ -82,6 +82,14 @@ export interface SpendConfig {
|
|
|
82
82
|
maxSessionUsd?: number;
|
|
83
83
|
/** Per-label budgets, each gated against that label's own spend. */
|
|
84
84
|
byLabel?: Record<string, number>;
|
|
85
|
+
/**
|
|
86
|
+
* Money budgets per source, in dollars — the fleet's version of `byLabel`.
|
|
87
|
+
* A source is a named service from the top-level `sources` block, and a
|
|
88
|
+
* budget written here fails `profile --by-source` when that service alone
|
|
89
|
+
* crosses it, with the failing service named rather than a total that
|
|
90
|
+
* hides which.
|
|
91
|
+
*/
|
|
92
|
+
bySource?: Record<string, number>;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
export interface TrazumConfig {
|
|
@@ -120,6 +128,14 @@ export interface TrazumConfig {
|
|
|
120
128
|
* workload against its own limit in the same run. A flag still wins over
|
|
121
129
|
* the config, as everywhere in this tool.
|
|
122
130
|
*/
|
|
131
|
+
/**
|
|
132
|
+
* The fleet: named services, each a list of glob patterns over usage-log
|
|
133
|
+
* paths. `profile --by-source` builds one report per source plus a rollup,
|
|
134
|
+
* assigning each file to the most specific matching pattern — the same
|
|
135
|
+
* precedence rule the budget patterns use. A file matching no source is
|
|
136
|
+
* named in the output rather than silently joining no report.
|
|
137
|
+
*/
|
|
138
|
+
sources?: Record<string, string[]>;
|
|
123
139
|
spend?: SpendConfig;
|
|
124
140
|
/**
|
|
125
141
|
* Findings as policy: a gate failure the team has looked at and decided to
|
|
@@ -169,6 +185,7 @@ export const CONFIG_KEYS = [
|
|
|
169
185
|
'budgets',
|
|
170
186
|
'labels',
|
|
171
187
|
'spend',
|
|
188
|
+
'sources',
|
|
172
189
|
'waive',
|
|
173
190
|
'maxGrowth',
|
|
174
191
|
'baseline',
|
|
@@ -178,7 +195,7 @@ export const CONFIG_KEYS = [
|
|
|
178
195
|
|
|
179
196
|
export const CONFIG_BASELINE_KEYS = ['path', 'maxGrowthTokens', 'maxGrowthPct'] as const;
|
|
180
197
|
|
|
181
|
-
export const CONFIG_SPEND_KEYS = ['maxUsd', 'maxDayUsd', 'maxSessionUsd', 'byLabel'] as const;
|
|
198
|
+
export const CONFIG_SPEND_KEYS = ['maxUsd', 'maxDayUsd', 'maxSessionUsd', 'byLabel', 'bySource'] as const;
|
|
182
199
|
|
|
183
200
|
export const CONFIG_WAIVE_KEYS = ['gate', 'reason', 'until'] as const;
|
|
184
201
|
|
|
@@ -385,6 +402,19 @@ function parseSpend(raw: unknown, source: string): SpendConfig {
|
|
|
385
402
|
if (raw.maxSessionUsd !== undefined) {
|
|
386
403
|
spend.maxSessionUsd = requireNonNegativeNumber(raw.maxSessionUsd, 'spend.maxSessionUsd', source);
|
|
387
404
|
}
|
|
405
|
+
if (raw.bySource !== undefined) {
|
|
406
|
+
if (!isPlainObject(raw.bySource)) {
|
|
407
|
+
throw new ConfigError('"spend.bySource" must be an object', source);
|
|
408
|
+
}
|
|
409
|
+
const bySource: Record<string, number> = {};
|
|
410
|
+
for (const [name, value] of Object.entries(raw.bySource)) {
|
|
411
|
+
if (name.trim().length === 0) {
|
|
412
|
+
throw new ConfigError('"spend.bySource" has an empty source name', source);
|
|
413
|
+
}
|
|
414
|
+
bySource[name] = requireNonNegativeNumber(value, `spend.bySource["${name}"]`, source);
|
|
415
|
+
}
|
|
416
|
+
spend.bySource = bySource;
|
|
417
|
+
}
|
|
388
418
|
if (raw.byLabel !== undefined) {
|
|
389
419
|
if (!isPlainObject(raw.byLabel)) {
|
|
390
420
|
throw new ConfigError('"spend.byLabel" must be an object', source);
|
|
@@ -401,6 +431,32 @@ function parseSpend(raw: unknown, source: string): SpendConfig {
|
|
|
401
431
|
return spend;
|
|
402
432
|
}
|
|
403
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Validates the `sources` block: named services, each a non-empty list of
|
|
436
|
+
* glob patterns. Patterns are not checked against a filesystem here — a
|
|
437
|
+
* config file is validated wherever it is read, browser included, and which
|
|
438
|
+
* files exist is the CLI's question at run time.
|
|
439
|
+
*/
|
|
440
|
+
function parseSources(raw: unknown, source: string): Record<string, string[]> {
|
|
441
|
+
if (!isPlainObject(raw)) throw new ConfigError('"sources" must be an object', source);
|
|
442
|
+
const sources: Record<string, string[]> = {};
|
|
443
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
444
|
+
if (name.trim().length === 0) {
|
|
445
|
+
throw new ConfigError('"sources" has an empty source name', source);
|
|
446
|
+
}
|
|
447
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
448
|
+
throw new ConfigError(`"sources.${name}" must be a non-empty array of glob patterns`, source);
|
|
449
|
+
}
|
|
450
|
+
for (const pattern of value) {
|
|
451
|
+
if (typeof pattern !== 'string' || pattern.trim().length === 0) {
|
|
452
|
+
throw new ConfigError(`"sources.${name}" has an empty pattern`, source);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
sources[name] = value as string[];
|
|
456
|
+
}
|
|
457
|
+
return sources;
|
|
458
|
+
}
|
|
459
|
+
|
|
404
460
|
/**
|
|
405
461
|
* Validates the `waive` list.
|
|
406
462
|
*
|
|
@@ -597,6 +653,7 @@ export function parseConfig(raw: string, source = CONFIG_FILENAME): TrazumConfig
|
|
|
597
653
|
if (document.budgets !== undefined) config.budgets = parseBudgets(document.budgets, source);
|
|
598
654
|
if (document.labels !== undefined) config.labels = parseLabels(document.labels, source);
|
|
599
655
|
if (document.spend !== undefined) config.spend = parseSpend(document.spend, source);
|
|
656
|
+
if (document.sources !== undefined) config.sources = parseSources(document.sources, source);
|
|
600
657
|
if (document.waive !== undefined) config.waive = parseWaive(document.waive, source);
|
|
601
658
|
if (document.baseline !== undefined) {
|
|
602
659
|
config.baseline = parseBaselineConfig(document.baseline, source);
|
package/src/fleet.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twelve services, one rollup, and the one that is actually bleeding.
|
|
3
|
+
*
|
|
4
|
+
* `profile` merges a directory of logs into one bill, which is right for one
|
|
5
|
+
* service and wrong for a fleet: the merged report hides which service the
|
|
6
|
+
* money is coming from, per-service budgets cannot exist at all, and the
|
|
7
|
+
* findings a *comparison between services* could make are invisible — the
|
|
8
|
+
* same workload on Opus in one team and Haiku in another is a decision
|
|
9
|
+
* somebody should get to see, and no single merged total shows it.
|
|
10
|
+
*
|
|
11
|
+
* This module does the fleet arithmetic on reports the caller already built.
|
|
12
|
+
* It reads no files and runs no globs against a filesystem — the caller hands
|
|
13
|
+
* it file names and per-source reports, so the module stays browser-safe and
|
|
14
|
+
* the CLI keeps its monopoly on I/O.
|
|
15
|
+
*
|
|
16
|
+
* **The rollup refuses to average what it cannot compare.** Two sources whose
|
|
17
|
+
* logs cover different periods can be *summed* — a total is a total — but a
|
|
18
|
+
* share of that total is not a comparison of rates, and the module says which
|
|
19
|
+
* sources cover which days rather than letting a 3-day log look cheap beside
|
|
20
|
+
* a 30-day one.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { mostSpecificMatch } from './glob.js';
|
|
24
|
+
import { UNLABELLED } from './usage.js';
|
|
25
|
+
import type { UsageProfileReport } from './usage.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Which source each file belongs to.
|
|
29
|
+
*
|
|
30
|
+
* Assignment is by the most specific matching glob, so `services/api/**`
|
|
31
|
+
* beats `services/**` on the same file — the same tie-break the budget
|
|
32
|
+
* patterns use, because two rules for pattern precedence in one tool is one
|
|
33
|
+
* rule too many. Files matching no source are returned rather than dropped:
|
|
34
|
+
* a log that silently joined no report would be spend missing from every
|
|
35
|
+
* bill, which is the flattering omission this repository refuses everywhere.
|
|
36
|
+
*/
|
|
37
|
+
export function assignSources(
|
|
38
|
+
files: string[],
|
|
39
|
+
sources: Record<string, string[]>,
|
|
40
|
+
): { bySource: Map<string, string[]>; unmatched: string[] } {
|
|
41
|
+
const bySource = new Map<string, string[]>();
|
|
42
|
+
const unmatched: string[] = [];
|
|
43
|
+
|
|
44
|
+
// Flatten to (pattern, source) pairs so specificity decides across sources.
|
|
45
|
+
const patterns: { pattern: string; source: string }[] = [];
|
|
46
|
+
for (const [source, globs] of Object.entries(sources)) {
|
|
47
|
+
for (const pattern of globs) patterns.push({ pattern, source });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const best = mostSpecificMatch(
|
|
52
|
+
patterns.map((p) => p.pattern),
|
|
53
|
+
file,
|
|
54
|
+
);
|
|
55
|
+
if (best === null) {
|
|
56
|
+
unmatched.push(file);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const source = patterns.find((p) => p.pattern === best)!.source;
|
|
60
|
+
const list = bySource.get(source) ?? [];
|
|
61
|
+
list.push(file);
|
|
62
|
+
bySource.set(source, list);
|
|
63
|
+
}
|
|
64
|
+
return { bySource, unmatched };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface FleetSource {
|
|
68
|
+
name: string;
|
|
69
|
+
report: UsageProfileReport;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface FleetRollup {
|
|
73
|
+
/** Sum over every source. A total is a total, whatever the spans. */
|
|
74
|
+
totalUsd: number;
|
|
75
|
+
calls: number;
|
|
76
|
+
/** Every source, dearest first, with its share of the fleet's total. */
|
|
77
|
+
sources: { name: string; usd: number; calls: number; share: number; spanDays: number | null }[];
|
|
78
|
+
/**
|
|
79
|
+
* The one that is actually bleeding: the dearest source, with its share —
|
|
80
|
+
* or null when the fleet spent nothing, because "nothing is bleeding" and
|
|
81
|
+
* "the worst of nothing" are different statements.
|
|
82
|
+
*/
|
|
83
|
+
worst: { name: string; usd: number; share: number } | null;
|
|
84
|
+
/**
|
|
85
|
+
* True when the sources' logs cover meaningfully different periods (their
|
|
86
|
+
* spans differ by more than one day, or some carry no clock at all). Shares
|
|
87
|
+
* of the total remain valid — they are shares of a sum — but reading them
|
|
88
|
+
* as *rate* comparisons is exactly the mistake this flag exists to stop,
|
|
89
|
+
* and every rendering states it when set.
|
|
90
|
+
*/
|
|
91
|
+
mismatchedSpans: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The same workload label running on different models in different sources
|
|
94
|
+
* — one team on Opus, another on Haiku, same job. A merged bill renders
|
|
95
|
+
* this invisible: the label's slices coexist with no seam. Only splits
|
|
96
|
+
* where both sides carry real spend are reported, dearest gap first.
|
|
97
|
+
*/
|
|
98
|
+
splitBrains: {
|
|
99
|
+
label: string;
|
|
100
|
+
sources: { name: string; model: string; usd: number }[];
|
|
101
|
+
}[];
|
|
102
|
+
/**
|
|
103
|
+
* Sources where caching lost money while the fleet's aggregate paid off.
|
|
104
|
+
* An aggregate verdict is the flattering rendering when three sources are
|
|
105
|
+
* quietly underwater; each is named with its own delta. Sources whose
|
|
106
|
+
* verdict matches the aggregate are not listed — this is the exception
|
|
107
|
+
* report, not the census.
|
|
108
|
+
*/
|
|
109
|
+
cacheUnderwater: { name: string; deltaUsd: number }[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Span length in days, or null when the report has no clock. */
|
|
113
|
+
function spanDaysOf(report: UsageProfileReport): number | null {
|
|
114
|
+
return report.span === null ? null : (report.span.toMs - report.span.fromMs) / 86_400_000;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function fleetRollup(
|
|
118
|
+
sources: FleetSource[],
|
|
119
|
+
options: {
|
|
120
|
+
/**
|
|
121
|
+
* Per-source cache verdict delta, positive meaning caching added money to
|
|
122
|
+
* the bill — the caller computes it with `cacheEconomics` because that
|
|
123
|
+
* module owns the counterfactual, and this one must not restate it.
|
|
124
|
+
*/
|
|
125
|
+
cacheDeltas?: Map<string, number>;
|
|
126
|
+
/** The fleet-wide delta under the same convention. */
|
|
127
|
+
aggregateCacheDelta?: number;
|
|
128
|
+
} = {},
|
|
129
|
+
): FleetRollup {
|
|
130
|
+
const rows = sources
|
|
131
|
+
.map((s) => ({
|
|
132
|
+
name: s.name,
|
|
133
|
+
usd: s.report.total.totalUsd,
|
|
134
|
+
calls: s.report.total.calls,
|
|
135
|
+
spanDays: spanDaysOf(s.report),
|
|
136
|
+
}))
|
|
137
|
+
.sort((a, b) => b.usd - a.usd);
|
|
138
|
+
|
|
139
|
+
const totalUsd = rows.reduce((sum, r) => sum + r.usd, 0);
|
|
140
|
+
const calls = rows.reduce((sum, r) => sum + r.calls, 0);
|
|
141
|
+
const withShare = rows.map((r) => ({
|
|
142
|
+
...r,
|
|
143
|
+
share: totalUsd > 0 ? r.usd / totalUsd : 0,
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
const spans = rows.map((r) => r.spanDays);
|
|
147
|
+
const known = spans.filter((d): d is number => d !== null);
|
|
148
|
+
const mismatchedSpans =
|
|
149
|
+
spans.some((d) => d === null && known.length > 0) ||
|
|
150
|
+
(known.length > 1 && Math.max(...known) - Math.min(...known) > 1);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Split brains: one label, different models, different sources. Judged on
|
|
154
|
+
* each source's *dearest* model for the label, so a stray experiment call
|
|
155
|
+
* does not report a team as migrated.
|
|
156
|
+
*/
|
|
157
|
+
const labelModel = new Map<string, Map<string, { model: string; usd: number }>>();
|
|
158
|
+
for (const source of sources) {
|
|
159
|
+
for (const slice of source.report.byLabelAndModel) {
|
|
160
|
+
if (slice.label === UNLABELLED) continue;
|
|
161
|
+
const perSource = labelModel.get(slice.label) ?? new Map();
|
|
162
|
+
const current = perSource.get(source.name);
|
|
163
|
+
if (current === undefined || slice.breakdown.totalUsd > current.usd) {
|
|
164
|
+
perSource.set(source.name, { model: slice.model, usd: slice.breakdown.totalUsd });
|
|
165
|
+
}
|
|
166
|
+
labelModel.set(slice.label, perSource);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const splitBrains: FleetRollup['splitBrains'] = [];
|
|
170
|
+
for (const [label, perSource] of labelModel) {
|
|
171
|
+
if (perSource.size < 2) continue;
|
|
172
|
+
const models = new Set([...perSource.values()].map((v) => v.model));
|
|
173
|
+
if (models.size < 2) continue;
|
|
174
|
+
const list = [...perSource.entries()]
|
|
175
|
+
.map(([name, v]) => ({ name, model: v.model, usd: v.usd }))
|
|
176
|
+
.filter((v) => v.usd > 0)
|
|
177
|
+
.sort((a, b) => b.usd - a.usd);
|
|
178
|
+
if (new Set(list.map((v) => v.model)).size < 2) continue;
|
|
179
|
+
splitBrains.push({ label, sources: list });
|
|
180
|
+
}
|
|
181
|
+
splitBrains.sort(
|
|
182
|
+
(a, b) =>
|
|
183
|
+
b.sources.reduce((s, v) => s + v.usd, 0) - a.sources.reduce((s, v) => s + v.usd, 0),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Cache underwater: only meaningful when the aggregate paid off — when the
|
|
188
|
+
* aggregate itself lost money, the whole-fleet report already shouts and
|
|
189
|
+
* naming each source would repeat it in pieces.
|
|
190
|
+
*/
|
|
191
|
+
const cacheUnderwater: FleetRollup['cacheUnderwater'] = [];
|
|
192
|
+
if (
|
|
193
|
+
options.cacheDeltas !== undefined &&
|
|
194
|
+
options.aggregateCacheDelta !== undefined &&
|
|
195
|
+
options.aggregateCacheDelta <= 0
|
|
196
|
+
) {
|
|
197
|
+
for (const [name, deltaUsd] of options.cacheDeltas) {
|
|
198
|
+
if (deltaUsd > 0) cacheUnderwater.push({ name, deltaUsd });
|
|
199
|
+
}
|
|
200
|
+
cacheUnderwater.sort((a, b) => b.deltaUsd - a.deltaUsd);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
totalUsd,
|
|
205
|
+
calls,
|
|
206
|
+
sources: withShare,
|
|
207
|
+
worst: totalUsd > 0 ? { name: rows[0]!.name, usd: rows[0]!.usd, share: withShare[0]!.share } : null,
|
|
208
|
+
mismatchedSpans,
|
|
209
|
+
splitBrains,
|
|
210
|
+
cacheUnderwater,
|
|
211
|
+
};
|
|
212
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -43,6 +43,10 @@ export type { AgainstDriver } from './against.js';
|
|
|
43
43
|
export { coverageDrift, COVERAGE_FIELDS, COVERAGE_DRIFT_MIN } from './coverage-drift.js';
|
|
44
44
|
export { explainGateFailure, gateMargin, GATE_MARGIN_TIGHT } from './gate-explain.js';
|
|
45
45
|
export { measuredUsage, labelCoverage, MIN_SCALE_DAYS, SCALE_TO_DAYS } from './measured-profile.js';
|
|
46
|
+
export { assignSources, fleetRollup } from './fleet.js';
|
|
47
|
+
export { buildPlan, planLabelName } from './plan.js';
|
|
48
|
+
export type { PlanAction, PlanActionKind, PlanAssumption, PlanDocument } from './plan.js';
|
|
49
|
+
export type { FleetSource, FleetRollup } from './fleet.js';
|
|
46
50
|
export type { MeasuredUsage, LabelCoverage } from './measured-profile.js';
|
|
47
51
|
export type { GateExplanation } from './gate-explain.js';
|
|
48
52
|
export type { CoverageDrift, CoverageField } from './coverage-drift.js';
|