@trazum/core 2.3.0 → 2.4.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.
Files changed (40) hide show
  1. package/dist/anthropic-cost.d.ts +173 -0
  2. package/dist/anthropic-cost.d.ts.map +1 -0
  3. package/dist/anthropic-cost.js +222 -0
  4. package/dist/anthropic-cost.js.map +1 -0
  5. package/dist/anthropic-usage.d.ts +192 -0
  6. package/dist/anthropic-usage.d.ts.map +1 -0
  7. package/dist/anthropic-usage.js +260 -0
  8. package/dist/anthropic-usage.js.map +1 -0
  9. package/dist/claude-code.d.ts +29 -1
  10. package/dist/claude-code.d.ts.map +1 -1
  11. package/dist/claude-code.js +33 -1
  12. package/dist/claude-code.js.map +1 -1
  13. package/dist/index.d.ts +11 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +5 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/openai-cost.d.ts +62 -0
  18. package/dist/openai-cost.d.ts.map +1 -0
  19. package/dist/openai-cost.js +151 -0
  20. package/dist/openai-cost.js.map +1 -0
  21. package/dist/openai-usage.d.ts +159 -0
  22. package/dist/openai-usage.d.ts.map +1 -0
  23. package/dist/openai-usage.js +281 -0
  24. package/dist/openai-usage.js.map +1 -0
  25. package/dist/openrouter-activity.d.ts +103 -0
  26. package/dist/openrouter-activity.d.ts.map +1 -0
  27. package/dist/openrouter-activity.js +169 -0
  28. package/dist/openrouter-activity.js.map +1 -0
  29. package/dist/pricing.d.ts.map +1 -1
  30. package/dist/pricing.js +25 -3
  31. package/dist/pricing.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/anthropic-cost.ts +303 -0
  34. package/src/anthropic-usage.ts +363 -0
  35. package/src/claude-code.ts +62 -2
  36. package/src/index.ts +19 -1
  37. package/src/openai-cost.ts +170 -0
  38. package/src/openai-usage.ts +353 -0
  39. package/src/openrouter-activity.ts +221 -0
  40. package/src/pricing.ts +27 -3
@@ -0,0 +1,173 @@
1
+ /**
2
+ * What the provider billed, beside what Trazum computed.
3
+ *
4
+ * The one figure nobody else gives you. Every other door here answers *what
5
+ * did this usage cost at these rates*; the provider answers *what did we
6
+ * charge you*, and the two are different questions that produce different
7
+ * numbers. The gap between them is the interesting part, and until this file
8
+ * there was nowhere to look at it.
9
+ *
10
+ * ## Why the two are never added
11
+ *
12
+ * `docs/commands.md` already states the rule for LiteLLM's `total_cost`: a
13
+ * provider-billed figure is printed **beside** Trazum's and never merged into
14
+ * it, because two price tables summed into one number is how a report becomes
15
+ * quietly wrong. This file is that rule with arithmetic attached. Nothing here
16
+ * corrects one figure with the other, and nothing here decides which is right:
17
+ * they are two measurements of the same window, and the reader is the one
18
+ * entitled to say which they trust.
19
+ *
20
+ * ## The unit, which is the trap
21
+ *
22
+ * `amount` is **a decimal string in the currency's lowest unit**: `"123.45"`
23
+ * in USD is $1.2345, not $123.45. A reader of this API who takes the field for
24
+ * dollars overstates a bill by a hundred times, and the figure looks entirely
25
+ * plausible on a large organisation. It is divided by a hundred here, once, at
26
+ * the end, and a test asserts the factor against the schema's own example.
27
+ *
28
+ * `currency` is documented as always `USD` today. It is still checked: a
29
+ * converter that assumed it would be silently wrong the day it changes, and
30
+ * summing two currencies into one total is the same failure as summing two
31
+ * price tables. Anything but USD is refused rather than converted, because a
32
+ * conversion needs a rate and inventing one is the sin this product is built
33
+ * against.
34
+ *
35
+ * ## What the decomposition is for
36
+ *
37
+ * A difference nobody can attribute is a number that starts an argument. This
38
+ * report can attribute most of one, when it is fetched with
39
+ * `group_by[]=description`:
40
+ *
41
+ * - **`cost_type` other than `tokens`** — web search, code execution, session
42
+ * usage. Trazum prices tokens and nothing else, so every cent of these is
43
+ * billed money it never claimed to cover. Named rather than blamed.
44
+ * - **`service_tier: "batch"`** — billed at a discount `from-anthropic`
45
+ * deliberately leaves out rather than misprice at a catalogue rate.
46
+ *
47
+ * What is left after both is the **remainder**, and it is the only figure here
48
+ * worth arguing about: the same standard-tier tokens, priced two ways, or
49
+ * usage the log never saw. A tool that folded the remainder into one of the
50
+ * explanations would be hiding the only thing it was built to show.
51
+ *
52
+ * Without `group_by[]=description` none of that decomposition exists —
53
+ * `cost_type`, `service_tier` and `model` are all `null` — and the reading
54
+ * says so rather than reporting a remainder that is really the whole
55
+ * difference wearing a smaller name.
56
+ */
57
+ /**
58
+ * What a provider charged, in the terms `reconcile` needs and no more.
59
+ *
60
+ * Two providers' reports read into it, and the difference between them is
61
+ * confined to the files that read them: what the money was for, whether
62
+ * batch can be told apart, which window, which currency.
63
+ */
64
+ export interface BilledReading {
65
+ /** Everything billed in the window, in dollars. */
66
+ usd: number;
67
+ /** Billed for something no token rate covers: web search, code execution. */
68
+ notTokensUsd: number;
69
+ /** Billed at the batch tier, which a catalogue rate is not the rate for. */
70
+ batchUsd: number;
71
+ /**
72
+ * Whether this report can name a batch charge at all.
73
+ *
74
+ * Anthropic's can, once grouped by description. OpenAI's never does, so a
75
+ * batch discount there sits inside the remainder and the reader is told.
76
+ */
77
+ batchSeparable: boolean;
78
+ /**
79
+ * Whether the report was fetched with the grouping that names each row.
80
+ *
81
+ * Without it nothing on a row says what the money was for, so neither
82
+ * figure above can be separated from the total and a reconciliation can
83
+ * only report a difference it cannot attribute.
84
+ */
85
+ described: boolean;
86
+ /** The window the buckets actually cover, or `null` when there are none. */
87
+ window: {
88
+ fromMs: number;
89
+ toMs: number;
90
+ } | null;
91
+ /** Currencies seen that were not USD. Summing across them is refused. */
92
+ otherCurrencies: string[];
93
+ }
94
+ /** What Anthropic charged, read from its own report. */
95
+ export interface AnthropicCostReading extends BilledReading {
96
+ buckets: number;
97
+ rows: number;
98
+ /** Rows whose `amount` was not a number. Counted, never read as zero. */
99
+ unreadableAmount: number;
100
+ /** `has_more`: one page of several, so the billed figure is understated. */
101
+ truncated: boolean;
102
+ /** The input was not the JSON this endpoint returns. */
103
+ unparseable: boolean;
104
+ }
105
+ /** Two measurements of one window, and what separates them. */
106
+ export interface Reconciliation {
107
+ computedUsd: number;
108
+ billedUsd: number;
109
+ /** Billed minus computed. Positive means the provider charged more. */
110
+ differenceUsd: number;
111
+ /** The part of the difference the report itself accounts for. */
112
+ notTokensUsd: number;
113
+ batchUsd: number;
114
+ /**
115
+ * What neither figure explains: the same standard-tier tokens priced two
116
+ * ways, or usage the log never saw. The only number here worth arguing
117
+ * about, and never folded into the two above.
118
+ */
119
+ remainderUsd: number;
120
+ /** `false` when the report was not grouped in a way that names its rows. */
121
+ attributable: boolean;
122
+ /** `false` when this provider's report cannot name a batch charge at all. */
123
+ batchSeparable: boolean;
124
+ /** Why the two are not comparable, when they are not. */
125
+ refusal: {
126
+ reason: 'no-billed-window';
127
+ } | {
128
+ reason: 'window-not-covered';
129
+ computed: {
130
+ fromMs: number;
131
+ toMs: number;
132
+ };
133
+ billed: {
134
+ fromMs: number;
135
+ toMs: number;
136
+ };
137
+ } | {
138
+ reason: 'other-currency';
139
+ currencies: string[];
140
+ } | null;
141
+ }
142
+ /**
143
+ * Read a cost report. Pure, like every measuring function here, and for the
144
+ * same reason `anthropic-usage.ts` gives: the credential that fetches this is
145
+ * an admin credential, and this project holds none.
146
+ */
147
+ export declare function anthropicCostReport(text: string): AnthropicCostReading;
148
+ /**
149
+ * Set what Trazum computed beside what the provider billed.
150
+ *
151
+ * @param computed what Trazum priced, and the window it priced.
152
+ *
153
+ * **The windows must line up or this refuses.** A receipt for September set
154
+ * against a bill for August is a wrong number under a right title, which is
155
+ * the failure this whole product is arranged against. The billed window has
156
+ * to contain the computed one — daily buckets are wider than a receipt's span
157
+ * by construction, so containment rather than equality is the honest test —
158
+ * and a report that starts after the first call it is being compared with is
159
+ * missing money nobody would see was missing.
160
+ */
161
+ export declare function reconcile(computed: {
162
+ usd: number;
163
+ fromMs: number;
164
+ toMs: number;
165
+ }, billed: BilledReading): Reconciliation;
166
+ /**
167
+ * Whether this text is a cost report rather than some other JSON.
168
+ *
169
+ * `amount` with `currency` beside it, in a document with buckets: the usage
170
+ * report has neither, and a Messages response has no buckets.
171
+ */
172
+ export declare function looksLikeAnthropicCost(text: string, prefixBytes?: number): boolean;
173
+ //# sourceMappingURL=anthropic-cost.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic-cost.d.ts","sourceRoot":"","sources":["../src/anthropic-cost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB;;;;;;OAMG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB,4EAA4E;IAC5E,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChD,yEAAyE;IACzE,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,wDAAwD;AACxD,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,gBAAgB,EAAE,MAAM,CAAC;IACzB,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAC;IACnB,wDAAwD;IACxD,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,aAAa,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,YAAY,EAAE,OAAO,CAAC;IACtB,6EAA6E;IAC7E,cAAc,EAAE,OAAO,CAAC;IACxB,yDAAyD;IACzD,OAAO,EACH;QAAE,MAAM,EAAE,kBAAkB,CAAA;KAAE,GAC9B;QAAE,MAAM,EAAE,oBAAoB,CAAC;QAAC,QAAQ,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,MAAM,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GACtH;QAAE,MAAM,EAAE,gBAAgB,CAAC;QAAC,UAAU,EAAE,MAAM,EAAE,CAAA;KAAE,GAClD,IAAI,CAAC;CACV;AAOD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,CA4FtE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACvD,MAAM,EAAE,aAAa,GACpB,cAAc,CAuChB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,SAAO,GAAG,OAAO,CAIhF"}
@@ -0,0 +1,222 @@
1
+ /**
2
+ * What the provider billed, beside what Trazum computed.
3
+ *
4
+ * The one figure nobody else gives you. Every other door here answers *what
5
+ * did this usage cost at these rates*; the provider answers *what did we
6
+ * charge you*, and the two are different questions that produce different
7
+ * numbers. The gap between them is the interesting part, and until this file
8
+ * there was nowhere to look at it.
9
+ *
10
+ * ## Why the two are never added
11
+ *
12
+ * `docs/commands.md` already states the rule for LiteLLM's `total_cost`: a
13
+ * provider-billed figure is printed **beside** Trazum's and never merged into
14
+ * it, because two price tables summed into one number is how a report becomes
15
+ * quietly wrong. This file is that rule with arithmetic attached. Nothing here
16
+ * corrects one figure with the other, and nothing here decides which is right:
17
+ * they are two measurements of the same window, and the reader is the one
18
+ * entitled to say which they trust.
19
+ *
20
+ * ## The unit, which is the trap
21
+ *
22
+ * `amount` is **a decimal string in the currency's lowest unit**: `"123.45"`
23
+ * in USD is $1.2345, not $123.45. A reader of this API who takes the field for
24
+ * dollars overstates a bill by a hundred times, and the figure looks entirely
25
+ * plausible on a large organisation. It is divided by a hundred here, once, at
26
+ * the end, and a test asserts the factor against the schema's own example.
27
+ *
28
+ * `currency` is documented as always `USD` today. It is still checked: a
29
+ * converter that assumed it would be silently wrong the day it changes, and
30
+ * summing two currencies into one total is the same failure as summing two
31
+ * price tables. Anything but USD is refused rather than converted, because a
32
+ * conversion needs a rate and inventing one is the sin this product is built
33
+ * against.
34
+ *
35
+ * ## What the decomposition is for
36
+ *
37
+ * A difference nobody can attribute is a number that starts an argument. This
38
+ * report can attribute most of one, when it is fetched with
39
+ * `group_by[]=description`:
40
+ *
41
+ * - **`cost_type` other than `tokens`** — web search, code execution, session
42
+ * usage. Trazum prices tokens and nothing else, so every cent of these is
43
+ * billed money it never claimed to cover. Named rather than blamed.
44
+ * - **`service_tier: "batch"`** — billed at a discount `from-anthropic`
45
+ * deliberately leaves out rather than misprice at a catalogue rate.
46
+ *
47
+ * What is left after both is the **remainder**, and it is the only figure here
48
+ * worth arguing about: the same standard-tier tokens, priced two ways, or
49
+ * usage the log never saw. A tool that folded the remainder into one of the
50
+ * explanations would be hiding the only thing it was built to show.
51
+ *
52
+ * Without `group_by[]=description` none of that decomposition exists —
53
+ * `cost_type`, `service_tier` and `model` are all `null` — and the reading
54
+ * says so rather than reporting a remainder that is really the whole
55
+ * difference wearing a smaller name.
56
+ */
57
+ const asObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value)
58
+ ? value
59
+ : null;
60
+ /**
61
+ * Read a cost report. Pure, like every measuring function here, and for the
62
+ * same reason `anthropic-usage.ts` gives: the credential that fetches this is
63
+ * an admin credential, and this project holds none.
64
+ */
65
+ export function anthropicCostReport(text) {
66
+ const empty = {
67
+ usd: 0,
68
+ notTokensUsd: 0,
69
+ batchUsd: 0,
70
+ batchSeparable: false,
71
+ described: false,
72
+ window: null,
73
+ buckets: 0,
74
+ rows: 0,
75
+ otherCurrencies: [],
76
+ unreadableAmount: 0,
77
+ truncated: false,
78
+ unparseable: false,
79
+ };
80
+ let parsed;
81
+ try {
82
+ parsed = JSON.parse(text);
83
+ }
84
+ catch {
85
+ return { ...empty, unparseable: true };
86
+ }
87
+ const report = asObject(parsed);
88
+ if (report === null || !Array.isArray(report.data))
89
+ return { ...empty, unparseable: true };
90
+ /* Summed in cents and divided once, because summing a hundredth of a cent a
91
+ thousand times and dividing each time is a different number. */
92
+ let cents = 0;
93
+ let notTokensCents = 0;
94
+ let batchCents = 0;
95
+ let described = false;
96
+ let buckets = 0;
97
+ let rows = 0;
98
+ let unreadableAmount = 0;
99
+ let fromMs = null;
100
+ let toMs = null;
101
+ const otherCurrencies = new Set();
102
+ for (const entry of report.data) {
103
+ const bucket = asObject(entry);
104
+ if (bucket === null || typeof bucket.starting_at !== 'string')
105
+ continue;
106
+ buckets += 1;
107
+ const from = Date.parse(bucket.starting_at);
108
+ const to = typeof bucket.ending_at === 'string' ? Date.parse(bucket.ending_at) : Number.NaN;
109
+ if (Number.isFinite(from))
110
+ fromMs = fromMs === null ? from : Math.min(fromMs, from);
111
+ if (Number.isFinite(to))
112
+ toMs = toMs === null ? to : Math.max(toMs, to);
113
+ if (!Array.isArray(bucket.results))
114
+ continue;
115
+ for (const found of bucket.results) {
116
+ const result = asObject(found);
117
+ if (result === null)
118
+ continue;
119
+ rows += 1;
120
+ /* A currency this cannot sum stops it being summed at all, rather than
121
+ being quietly added to a dollar total. */
122
+ if (typeof result.currency === 'string' && result.currency !== 'USD') {
123
+ otherCurrencies.add(result.currency);
124
+ }
125
+ const amount = typeof result.amount === 'string' ? Number(result.amount) : Number.NaN;
126
+ if (!Number.isFinite(amount)) {
127
+ unreadableAmount += 1;
128
+ continue;
129
+ }
130
+ cents += amount;
131
+ if (typeof result.cost_type === 'string') {
132
+ described = true;
133
+ if (result.cost_type !== 'tokens')
134
+ notTokensCents += amount;
135
+ }
136
+ if (typeof result.service_tier === 'string') {
137
+ described = true;
138
+ if (result.service_tier === 'batch')
139
+ batchCents += amount;
140
+ }
141
+ }
142
+ }
143
+ return {
144
+ usd: cents / 100,
145
+ notTokensUsd: notTokensCents / 100,
146
+ batchUsd: batchCents / 100,
147
+ batchSeparable: described,
148
+ described,
149
+ window: fromMs === null || toMs === null ? null : { fromMs, toMs },
150
+ buckets,
151
+ rows,
152
+ otherCurrencies: [...otherCurrencies],
153
+ unreadableAmount,
154
+ truncated: report.has_more === true,
155
+ unparseable: false,
156
+ };
157
+ }
158
+ /**
159
+ * Set what Trazum computed beside what the provider billed.
160
+ *
161
+ * @param computed what Trazum priced, and the window it priced.
162
+ *
163
+ * **The windows must line up or this refuses.** A receipt for September set
164
+ * against a bill for August is a wrong number under a right title, which is
165
+ * the failure this whole product is arranged against. The billed window has
166
+ * to contain the computed one — daily buckets are wider than a receipt's span
167
+ * by construction, so containment rather than equality is the honest test —
168
+ * and a report that starts after the first call it is being compared with is
169
+ * missing money nobody would see was missing.
170
+ */
171
+ export function reconcile(computed, billed) {
172
+ const bare = {
173
+ computedUsd: computed.usd,
174
+ billedUsd: billed.usd,
175
+ differenceUsd: billed.usd - computed.usd,
176
+ notTokensUsd: billed.notTokensUsd,
177
+ batchUsd: billed.batchUsd,
178
+ remainderUsd: 0,
179
+ attributable: billed.described,
180
+ batchSeparable: billed.batchSeparable,
181
+ };
182
+ if (billed.otherCurrencies.length > 0) {
183
+ return { ...bare, refusal: { reason: 'other-currency', currencies: billed.otherCurrencies } };
184
+ }
185
+ if (billed.window === null)
186
+ return { ...bare, refusal: { reason: 'no-billed-window' } };
187
+ if (billed.window.fromMs > computed.fromMs || billed.window.toMs < computed.toMs) {
188
+ return {
189
+ ...bare,
190
+ refusal: {
191
+ reason: 'window-not-covered',
192
+ computed: { fromMs: computed.fromMs, toMs: computed.toMs },
193
+ billed: billed.window,
194
+ },
195
+ };
196
+ }
197
+ /*
198
+ The remainder is what is left when the report's own explanations are taken
199
+ out of the difference, and it is deliberately not clamped at zero. A
200
+ negative one is a real answer -- Trazum priced more than the provider
201
+ charged -- and reporting it as zero would hide a stale rate in exactly the
202
+ direction that costs somebody money.
203
+ */
204
+ return {
205
+ ...bare,
206
+ remainderUsd: bare.differenceUsd - billed.notTokensUsd - billed.batchUsd,
207
+ refusal: null,
208
+ };
209
+ }
210
+ /**
211
+ * Whether this text is a cost report rather than some other JSON.
212
+ *
213
+ * `amount` with `currency` beside it, in a document with buckets: the usage
214
+ * report has neither, and a Messages response has no buckets.
215
+ */
216
+ export function looksLikeAnthropicCost(text, prefixBytes = 8192) {
217
+ const head = text.slice(0, prefixBytes);
218
+ if (!head.includes('"amount"') || !head.includes('"currency"'))
219
+ return false;
220
+ return head.includes('"starting_at"') || head.includes('"has_more"');
221
+ }
222
+ //# sourceMappingURL=anthropic-cost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic-cost.js","sourceRoot":"","sources":["../src/anthropic-cost.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Anthropic's own usage report, read as a usage log.
3
+ *
4
+ * The fifth converter in the pattern `from-claude-code` started, and the first
5
+ * one that reads a **provider** rather than a tool sitting in front of one.
6
+ * Every other converter answers *what did the calls my proxy saw cost*; this
7
+ * one answers *what does the provider say my organisation used*, which is the
8
+ * question a finance team asks and the one no local log can answer, because a
9
+ * log only knows the machines it was written on.
10
+ *
11
+ * ## Why this takes a file and not a key
12
+ *
13
+ * The Usage & Cost API needs an **admin** credential: an `sk-ant-admin01-…`
14
+ * key, an `org:admin` OAuth token, or an unscoped personal or service account
15
+ * key. Workspace keys do not work. That credential can read every workspace's
16
+ * usage and manage the organisation's members and keys, which makes it exactly
17
+ * the kind of secret this project has spent its whole design not holding.
18
+ *
19
+ * So this function takes the **response**, not the credential. The operator
20
+ * runs one `curl` with their own key, in their own shell, and pipes the JSON
21
+ * here. Trazum never sees the key, never makes the request, and stays a pure
22
+ * function of text — which is also why this file can be tested without a
23
+ * network and why the deterministic core is still deterministic.
24
+ *
25
+ * ## The format is derived, not guessed
26
+ *
27
+ * Every field below is from the published response schema of
28
+ * `GET /v1/organizations/usage_report/messages`: `data[]` of time buckets with
29
+ * `starting_at`, `ending_at` and `results[]`, and each result carrying
30
+ * `uncached_input_tokens`, `cache_read_input_tokens`, `cache_creation`
31
+ * (`ephemeral_5m_input_tokens`, `ephemeral_1h_input_tokens`), `output_tokens`,
32
+ * `model`, `service_tier` and `server_tool_use.web_search_requests`.
33
+ *
34
+ * The cache shape needs no translation at all: `parseUsageLine` already reads
35
+ * `cache_creation.ephemeral_5m_input_tokens` and its 1-hour sibling, because
36
+ * that is the shape the Messages API itself returns. The one rename is
37
+ * `uncached_input_tokens` to `input_tokens`, and it is a rename rather than a
38
+ * sum: this report reports cached reads **beside** the uncached input rather
39
+ * than folded into it, which is the Anthropic shape and not the OpenAI one.
40
+ *
41
+ * ## What it refuses to price, and counts instead
42
+ *
43
+ * **A result with no model.** `model` is `null` unless the caller passed
44
+ * `group_by[]=model`. Nothing on a row without it says what answered, so
45
+ * nothing can price it. Refused, counted, and the count names the missing
46
+ * parameter — a converter that quietly returned an empty log would send
47
+ * somebody looking for a bug in their key.
48
+ *
49
+ * **A non-standard service tier.** Batch is billed at a discount and priority
50
+ * is not billed like either; a catalogue rate is the standard rate. Pricing a
51
+ * batch row from it would overstate a bill by half and look right doing it,
52
+ * which is this product's one unforgivable failure. Those rows are left out
53
+ * and counted, the same way an unpriceable line becomes a named gap rather
54
+ * than a guess.
55
+ *
56
+ * **Web search requests.** `server_tool_use.web_search_requests` is billed per
57
+ * request, not per token, so no token rate reaches it. Counted and named, so a
58
+ * total that is missing them says so.
59
+ *
60
+ * **A truncated report.** `has_more: true` means the answer is one page of
61
+ * several, and a bill built from one page is understated by however many pages
62
+ * were not asked for. Said out loud rather than left for somebody to notice in
63
+ * a figure.
64
+ *
65
+ * ## What deliberately does not cross
66
+ *
67
+ * `account_id`, `service_account_id` and `api_key_id` are read by nothing
68
+ * here. The first two name people; the third names a credential. None of them
69
+ * is a project name and none of them reaches the output.
70
+ *
71
+ * `workspace_id` is read only when the operator hands over a mapping, because
72
+ * a workspace id is not a project name and turning one into the other is a
73
+ * decision rather than an inference. `labelByWorkspace` is that decision,
74
+ * written down: the same shape `--label-by-cwd` established for transcripts,
75
+ * exact rather than prefixed, because an opaque identifier has no hierarchy
76
+ * to take the longest match of.
77
+ *
78
+ * ## The `null` that means two things, and how it is told apart
79
+ *
80
+ * The report's own schema says `workspace_id` is `null` **both** when the
81
+ * caller did not group by workspace **and** for the default workspace. One
82
+ * value, two meanings, and a mapping that guessed between them would put the
83
+ * default workspace's money on a label nobody chose or leave it off one they
84
+ * did.
85
+ *
86
+ * It is derived instead: if any result in the report carries a workspace id,
87
+ * the report was grouped, and a `null` there is the default workspace — so a
88
+ * rule written for `null` applies to it. If no result carries one, nothing
89
+ * distinguishes the two readings, `null` rules do not apply, and the
90
+ * conversion says the split was asked for and could not be made.
91
+ *
92
+ * ## The instant, and why it is the bucket's
93
+ *
94
+ * This report has no calls in it. A bucket is an interval and its usage is a
95
+ * sum over that interval, so every record made from it carries the bucket's
96
+ * `starting_at` — a day's usage at that day's start, an hour's at the hour's.
97
+ * Anything finer would be a precision the source does not have, and
98
+ * `bucket_width=1h` is there for an operator who wants it.
99
+ */
100
+ /**
101
+ * One workspace, and the project name the operator gives it.
102
+ *
103
+ * Matched exactly, never by prefix: a workspace id is opaque and two of them
104
+ * sharing leading characters share nothing at all. `null` names the default
105
+ * workspace, which is the only workspace the report identifies by absence.
106
+ */
107
+ export interface WorkspaceLabel {
108
+ workspace: string | null;
109
+ label: string;
110
+ }
111
+ /** One converted record, shaped exactly as `parseUsageLine` reads it. */
112
+ export interface AnthropicUsageRecord {
113
+ model: string;
114
+ ts: string;
115
+ label?: string;
116
+ usage: {
117
+ input_tokens: number;
118
+ output_tokens: number;
119
+ cache_read_input_tokens?: number;
120
+ cache_creation?: {
121
+ ephemeral_5m_input_tokens: number;
122
+ ephemeral_1h_input_tokens: number;
123
+ };
124
+ };
125
+ }
126
+ export interface AnthropicUsageConversion {
127
+ records: AnthropicUsageRecord[];
128
+ /** Time buckets in the report, including intervals with no usage. */
129
+ buckets: number;
130
+ /** Results that became records. */
131
+ rows: number;
132
+ /** Results with `model: null`, which nothing can price. */
133
+ unnamedModel: number;
134
+ /** Results at a tier a standard rate would misprice. */
135
+ nonStandardTier: number;
136
+ /**
137
+ * Whether any result named its tier at all.
138
+ *
139
+ * `false` means the report was not grouped by `service_tier`, so a batch row
140
+ * and a standard row are indistinguishable here and the discount, if any,
141
+ * is invisible. Not an error and not a refusal: most organisations run
142
+ * standard only. It is said because the alternative is a reader assuming a
143
+ * question was answered that was never asked.
144
+ */
145
+ tierNamed: boolean;
146
+ /** Server-side tool calls billed per request, which no token rate prices. */
147
+ webSearchRequests: number;
148
+ /** `has_more`: this is one page and the bill from it is understated. */
149
+ truncated: boolean;
150
+ /** Records whose label came from a workspace rule rather than `--label`. */
151
+ labelledByWorkspace: number;
152
+ /**
153
+ * Results carrying a workspace no rule named.
154
+ *
155
+ * They keep `--label` if there is one and go unattributed if there is not,
156
+ * which is the same fallback `--label-by-cwd` makes: a workspace nobody
157
+ * wrote a rule for is unattributed rather than attributed to a neighbour.
158
+ */
159
+ unruledWorkspace: number;
160
+ /**
161
+ * Rules were given and no result carried a workspace id.
162
+ *
163
+ * Either the report was not grouped by workspace or this organisation uses
164
+ * only the default one, and nothing here can tell those apart. The split
165
+ * asked for was not made, and that is said rather than left to be noticed.
166
+ */
167
+ workspaceNotGrouped: boolean;
168
+ /** The input was not the JSON this endpoint returns. */
169
+ unparseable: number;
170
+ }
171
+ /**
172
+ * @param label the project this usage belongs to, chosen by the operator.
173
+ *
174
+ * Optional, and absent means the records carry none — the unattributed bucket
175
+ * every other door already understands. The provider does not know this
176
+ * organisation's project names and neither does this file.
177
+ */
178
+ export declare function anthropicUsageRecords(text: string, options?: {
179
+ label?: string;
180
+ labelByWorkspace?: readonly WorkspaceLabel[];
181
+ }): AnthropicUsageConversion;
182
+ /**
183
+ * Whether this text is an Anthropic usage report rather than some other JSON.
184
+ *
185
+ * Three fields together, because each alone is too common to decide on:
186
+ * `uncached_input_tokens` is this endpoint's own name for a count every other
187
+ * shape calls something else, and a report always carries buckets with a
188
+ * `starting_at`. A sniffer that matched on `output_tokens` alone would claim
189
+ * every Messages response ever saved to a file.
190
+ */
191
+ export declare function looksLikeAnthropicUsage(text: string, prefixBytes?: number): boolean;
192
+ //# sourceMappingURL=anthropic-usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic-usage.d.ts","sourceRoot":"","sources":["../src/anthropic-usage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkGG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,yEAAyE;AACzE,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE;QACL,YAAY,EAAE,MAAM,CAAC;QACrB,aAAa,EAAE,MAAM,CAAC;QACtB,uBAAuB,CAAC,EAAE,MAAM,CAAC;QACjC,cAAc,CAAC,EAAE;YACf,yBAAyB,EAAE,MAAM,CAAC;YAClC,yBAAyB,EAAE,MAAM,CAAC;SACnC,CAAC;KACH,CAAC;CACH;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;;;;OAQG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,iBAAiB,EAAE,MAAM,CAAC;IAC1B,wEAAwE;IACxE,SAAS,EAAE,OAAO,CAAC;IACnB,4EAA4E;IAC5E,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;;OAMG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,mBAAmB,EAAE,OAAO,CAAC;IAC7B,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;CACrB;AAKD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;CAAO,GAC7E,wBAAwB,CAgK1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,SAAO,GAAG,OAAO,CAIjF"}