@trazum/core 1.39.0 → 1.41.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 +28 -0
- package/dist/connector.d.ts +227 -0
- package/dist/connector.d.ts.map +1 -0
- package/dist/connector.js +431 -0
- package/dist/connector.js.map +1 -0
- package/dist/history.d.ts +110 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +191 -0
- package/dist/history.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/package.json +1 -1
- package/src/connector.ts +616 -0
- package/src/history.ts +284 -0
- package/src/index.ts +20 -0
package/README.md
CHANGED
|
@@ -119,6 +119,34 @@ silently priced through, and each verdict says whether it fails a gate:
|
|
|
119
119
|
`not-arrived` always, `fields-stopped` too — a team must not pass on the
|
|
120
120
|
strength of its own log's silence — and a vanished workload never.
|
|
121
121
|
|
|
122
|
+
## The long run
|
|
123
|
+
|
|
124
|
+
`buildHistory(reports, plans)` turns stored reports into the series no
|
|
125
|
+
pairwise comparison can see: per-label spend, per-model share and cache
|
|
126
|
+
share per period, with consecutive movement named as runs (`MIN_RUN` rises
|
|
127
|
+
or falls at least — two is a comparison, one is noise) and the same plan
|
|
128
|
+
action appearing in two or more plans reported as a decision nobody is
|
|
129
|
+
executing. `storedReportFrom(name, parsed)` reads one stored `profile`
|
|
130
|
+
document into the slice history needs, returning null for anything else so
|
|
131
|
+
the caller names the file instead of absorbing it. No forecasts anywhere:
|
|
132
|
+
shapes are stated with their first and last values, and where they go next
|
|
133
|
+
is the reader's.
|
|
134
|
+
|
|
135
|
+
## The bill, from the provider
|
|
136
|
+
|
|
137
|
+
`normalizeAnthropicUsage(payload)` and `normalizeOpenAIUsage(payload)` turn a
|
|
138
|
+
usage API response into `UsageBucket`s — token sums per window and model, with
|
|
139
|
+
the two cache-write TTLs kept apart and a request count only where the
|
|
140
|
+
provider actually serves one (`null` otherwise, never zero).
|
|
141
|
+
`bucketedProfile(pull, { catalogue })` prices them, and
|
|
142
|
+
`bucketedCacheEconomics(report)` runs the same counterfactual `cacheEconomics`
|
|
143
|
+
runs per call. The result is deliberately its own shape rather than a
|
|
144
|
+
`UsageProfileReport` with holes in it, so no per-call finding can read a zero
|
|
145
|
+
this module wrote: every finding a sum cannot support is listed in
|
|
146
|
+
`unavailable` with why and what would unlock it. Anything unreadable in the
|
|
147
|
+
payload becomes a named `PullGap`, never a default of zero. Pure and
|
|
148
|
+
browser-safe — the fetch, the credentials and the pagination live in the CLI.
|
|
149
|
+
|
|
122
150
|
## Comparing two versions
|
|
123
151
|
|
|
124
152
|
```ts
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Your bill, read from the provider, without anybody exporting anything.
|
|
3
|
+
*
|
|
4
|
+
* Every command in this product reads a file somebody produced by hand, and
|
|
5
|
+
* the export step is where adoption dies: the person who would benefit most
|
|
6
|
+
* from a cost report is the person least likely to have a `usage.jsonl` lying
|
|
7
|
+
* around. Every provider that bills by the token also serves that data over an
|
|
8
|
+
* API, and this module turns those payloads into figures the rest of Trazum
|
|
9
|
+
* already knows how to reason about.
|
|
10
|
+
*
|
|
11
|
+
* **Pure, and in the core, so it is testable without a network.** The fetch,
|
|
12
|
+
* the credentials and the pagination live in the CLI — the same split
|
|
13
|
+
* `openrouterOverlay` has had since 1.13. Everything here is a transformation
|
|
14
|
+
* of a document the caller already holds.
|
|
15
|
+
*
|
|
16
|
+
* **The honest part is what the providers cannot tell you.** Usage APIs serve
|
|
17
|
+
* *aggregates*: tokens per bucket per model, and — depending on the provider —
|
|
18
|
+
* a request count or nothing at all. They do not serve per-call rows. That
|
|
19
|
+
* makes a whole class of Trazum's findings impossible on this source: the
|
|
20
|
+
* shape of the calls, the truncation retries, the conversations, the largest
|
|
21
|
+
* call's context pressure. Those findings need per-call data and no amount of
|
|
22
|
+
* arithmetic recovers them from a sum.
|
|
23
|
+
*
|
|
24
|
+
* So a connected report is a **restricted** report, and it is restricted out
|
|
25
|
+
* loud. It carries its own shape rather than a `UsageProfileReport` with holes
|
|
26
|
+
* in it, precisely so a per-call finding can never read a zero this module
|
|
27
|
+
* wrote and report "nothing found" about something nobody measured. Not
|
|
28
|
+
* recorded is not not-happened, at the level of the type system.
|
|
29
|
+
*/
|
|
30
|
+
import type { PricingCatalogue } from './pricing.js';
|
|
31
|
+
/**
|
|
32
|
+
* `per-call` sources serve one row per request and unlock every finding in
|
|
33
|
+
* the product. `bucketed` sources serve sums over a window.
|
|
34
|
+
*/
|
|
35
|
+
export type ConnectorGranularity = 'per-call' | 'bucketed';
|
|
36
|
+
/**
|
|
37
|
+
* A finding this source cannot support, why, and what would unlock it.
|
|
38
|
+
*
|
|
39
|
+
* Carried into the report and printed there. A restricted report that only
|
|
40
|
+
* omits things reads as a report that found nothing wrong.
|
|
41
|
+
*/
|
|
42
|
+
export interface UnavailableFinding {
|
|
43
|
+
finding: string;
|
|
44
|
+
because: string;
|
|
45
|
+
unlockedBy: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ConnectorDescriptor {
|
|
48
|
+
id: string;
|
|
49
|
+
displayName: string;
|
|
50
|
+
granularity: ConnectorGranularity;
|
|
51
|
+
/**
|
|
52
|
+
* Environment variables the CLI reads the credential from, in order.
|
|
53
|
+
*
|
|
54
|
+
* Named here so `trazum connect` can say exactly what it looked for when it
|
|
55
|
+
* finds nothing. Trazum stores no secret: a key lives in the environment or
|
|
56
|
+
* in a keychain the operating system owns, and never in this repository's
|
|
57
|
+
* config, cache or output.
|
|
58
|
+
*/
|
|
59
|
+
credentialEnv: readonly string[];
|
|
60
|
+
/** The narrowest key that works, so nobody hands this tool a wider one. */
|
|
61
|
+
keyKind: string;
|
|
62
|
+
/** Whether the source serves a request count, or only token sums. */
|
|
63
|
+
servesCallCounts: boolean;
|
|
64
|
+
/** Findings impossible on this source. */
|
|
65
|
+
unavailable: readonly UnavailableFinding[];
|
|
66
|
+
docs: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The two providers this release connects to, and the asymmetry between them
|
|
70
|
+
* that a report must not paper over.
|
|
71
|
+
*
|
|
72
|
+
* OpenAI's usage endpoint serves a request count per bucket; Anthropic's
|
|
73
|
+
* serves token sums without one. So a connected OpenAI report can say "$412
|
|
74
|
+
* over 9,004 calls" and a connected Anthropic report can only say "$412", and
|
|
75
|
+
* every per-call average is available on one and absent on the other. Printing
|
|
76
|
+
* a call count of zero, or dividing by a denominator that does not exist,
|
|
77
|
+
* would be this module inventing the number it is here to stop inventing.
|
|
78
|
+
*/
|
|
79
|
+
export declare const CONNECTORS: readonly ConnectorDescriptor[];
|
|
80
|
+
export declare function connectorFor(id: string): ConnectorDescriptor | null;
|
|
81
|
+
/**
|
|
82
|
+
* One provider bucket: a window, a model, and the tokens billed inside it.
|
|
83
|
+
*
|
|
84
|
+
* The cache-write TTL split is kept apart for the same reason `UsageBreakdown`
|
|
85
|
+
* keeps it apart — the two are billed at different multipliers, and a total
|
|
86
|
+
* that has lost the split cannot be repriced, only guessed at.
|
|
87
|
+
*/
|
|
88
|
+
export interface UsageBucket {
|
|
89
|
+
fromMs: number;
|
|
90
|
+
toMs: number;
|
|
91
|
+
model: string;
|
|
92
|
+
/** null when the provider serves no request count. Never zero for absent. */
|
|
93
|
+
calls: number | null;
|
|
94
|
+
inputTokens: number;
|
|
95
|
+
cacheReadTokens: number;
|
|
96
|
+
cacheWrite5mTokens: number;
|
|
97
|
+
cacheWrite1hTokens: number;
|
|
98
|
+
/** False when the provider reported writes without saying which TTL. */
|
|
99
|
+
writeTtlKnown: boolean;
|
|
100
|
+
outputTokens: number;
|
|
101
|
+
/** Whatever the provider grouped by beyond the model — workspace, key, tier. */
|
|
102
|
+
group: Record<string, string>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Something the pull did not get.
|
|
106
|
+
*
|
|
107
|
+
* A bill quietly short by an unknown amount is the failure this repository
|
|
108
|
+
* refuses everywhere it can occur, and a paginated API behind a rate limit is
|
|
109
|
+
* exactly where it occurs. Every gap is carried to the report and printed.
|
|
110
|
+
*/
|
|
111
|
+
export interface PullGap {
|
|
112
|
+
kind: 'rate-limited' | 'retention-boundary' | 'cursor-expired' | 'page-limit' | 'unreadable-entry' | 'unreadable-field';
|
|
113
|
+
detail: string;
|
|
114
|
+
}
|
|
115
|
+
export interface ConnectorPull {
|
|
116
|
+
provider: string;
|
|
117
|
+
granularity: ConnectorGranularity;
|
|
118
|
+
buckets: UsageBucket[];
|
|
119
|
+
/** The window the buckets actually cover, or null when none parsed. */
|
|
120
|
+
window: {
|
|
121
|
+
fromMs: number;
|
|
122
|
+
toMs: number;
|
|
123
|
+
} | null;
|
|
124
|
+
gaps: PullGap[];
|
|
125
|
+
unavailable: readonly UnavailableFinding[];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Anthropic's messages usage report.
|
|
129
|
+
*
|
|
130
|
+
* Shape: `{ data: [ { starting_at, ending_at, results: [ {...tokens, model} ] } ] }`.
|
|
131
|
+
* The fields read are the documented ones; anything unreadable is reported as
|
|
132
|
+
* a gap rather than defaulted to zero, because a zero here is a bill that is
|
|
133
|
+
* quietly smaller than the real one.
|
|
134
|
+
*/
|
|
135
|
+
export declare function normalizeAnthropicUsage(payload: unknown): ConnectorPull;
|
|
136
|
+
/**
|
|
137
|
+
* OpenAI's completions usage endpoint.
|
|
138
|
+
*
|
|
139
|
+
* Shape: `{ data: [ { start_time, end_time, results: [ { input_tokens,
|
|
140
|
+
* output_tokens, input_cached_tokens, num_model_requests, model } ] } ] }`.
|
|
141
|
+
*
|
|
142
|
+
* This one serves a request count, so every per-call average is available on
|
|
143
|
+
* it — and the report says so, rather than making both providers look alike.
|
|
144
|
+
*/
|
|
145
|
+
export declare function normalizeOpenAIUsage(payload: unknown): ConnectorPull;
|
|
146
|
+
export interface BucketedSlice {
|
|
147
|
+
model: string;
|
|
148
|
+
/** null when the source serves no request count. */
|
|
149
|
+
calls: number | null;
|
|
150
|
+
inputTokens: number;
|
|
151
|
+
cacheReadTokens: number;
|
|
152
|
+
cacheWriteTokens: number;
|
|
153
|
+
outputTokens: number;
|
|
154
|
+
inputUsd: number;
|
|
155
|
+
cacheReadUsd: number;
|
|
156
|
+
cacheWriteUsd: number;
|
|
157
|
+
outputUsd: number;
|
|
158
|
+
totalUsd: number;
|
|
159
|
+
/** What the cache-touched tokens would have cost as ordinary input. */
|
|
160
|
+
cachedTokensAtInputRateUsd: number;
|
|
161
|
+
/** Writes priced at the 1-hour rate, when the source did not state the TTL. */
|
|
162
|
+
cacheWriteUsdIfAssumed1h: number;
|
|
163
|
+
writeTtlKnown: boolean;
|
|
164
|
+
}
|
|
165
|
+
export interface BucketedReport {
|
|
166
|
+
schemaVersion: 1;
|
|
167
|
+
provider: string;
|
|
168
|
+
granularity: ConnectorGranularity;
|
|
169
|
+
span: {
|
|
170
|
+
fromMs: number;
|
|
171
|
+
toMs: number;
|
|
172
|
+
} | null;
|
|
173
|
+
total: {
|
|
174
|
+
totalUsd: number;
|
|
175
|
+
/** null when unknown — never zero, which would read as "no traffic". */
|
|
176
|
+
calls: number | null;
|
|
177
|
+
inputTokens: number;
|
|
178
|
+
cacheReadTokens: number;
|
|
179
|
+
cacheWriteTokens: number;
|
|
180
|
+
outputTokens: number;
|
|
181
|
+
};
|
|
182
|
+
byModel: BucketedSlice[];
|
|
183
|
+
/** Spend per UTC day, oldest first — the shape a total hides. */
|
|
184
|
+
byDay: {
|
|
185
|
+
day: string;
|
|
186
|
+
usd: number;
|
|
187
|
+
calls: number | null;
|
|
188
|
+
}[];
|
|
189
|
+
/** Models the catalogue could not price: named, with their tokens kept. */
|
|
190
|
+
unpricedModels: {
|
|
191
|
+
model: string;
|
|
192
|
+
inputTokens: number;
|
|
193
|
+
outputTokens: number;
|
|
194
|
+
}[];
|
|
195
|
+
gaps: PullGap[];
|
|
196
|
+
unavailable: readonly UnavailableFinding[];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Prices the buckets a connector pulled.
|
|
200
|
+
*
|
|
201
|
+
* Every figure here is the provider's own billed token count at the
|
|
202
|
+
* catalogue's rates — the same arithmetic `profile` does, over sums instead of
|
|
203
|
+
* rows. What it deliberately does not do is synthesise the per-call findings:
|
|
204
|
+
* they are listed as unavailable and left absent, so nothing downstream can
|
|
205
|
+
* read a zero this function wrote.
|
|
206
|
+
*/
|
|
207
|
+
export declare function bucketedProfile(pull: ConnectorPull, options: {
|
|
208
|
+
catalogue: PricingCatalogue;
|
|
209
|
+
on?: Date;
|
|
210
|
+
}): BucketedReport;
|
|
211
|
+
/**
|
|
212
|
+
* The cache verdict over a connected report.
|
|
213
|
+
*
|
|
214
|
+
* Same counterfactual `cacheEconomics` runs on a per-call report: what the
|
|
215
|
+
* cache-touched tokens cost, against what they would have cost as ordinary
|
|
216
|
+
* input. The worst case is carried separately for the same reason it is
|
|
217
|
+
* there — when the source did not state the write TTL, the cheaper rate was
|
|
218
|
+
* assumed for the headline and the verdict can move under the other one.
|
|
219
|
+
*/
|
|
220
|
+
export declare function bucketedCacheEconomics(report: BucketedReport): {
|
|
221
|
+
spentUsd: number;
|
|
222
|
+
withoutCachingUsd: number;
|
|
223
|
+
deltaUsd: number;
|
|
224
|
+
verdict: 'paid-off' | 'lost-money' | 'no-cache';
|
|
225
|
+
worstCaseVerdict: 'paid-off' | 'lost-money' | 'no-cache';
|
|
226
|
+
};
|
|
227
|
+
//# sourceMappingURL=connector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"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"}
|