@trazum/core 2.4.2 → 2.5.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/advisories.d.ts.map +1 -1
- package/dist/advisories.js +17 -10
- package/dist/advisories.js.map +1 -1
- package/dist/answer.d.ts.map +1 -1
- package/dist/answer.js +2 -1
- package/dist/answer.js.map +1 -1
- package/dist/anthropic-usage.d.ts +6 -0
- package/dist/anthropic-usage.d.ts.map +1 -1
- package/dist/anthropic-usage.js +1 -0
- package/dist/anthropic-usage.js.map +1 -1
- package/dist/billing.d.ts +20 -0
- package/dist/billing.d.ts.map +1 -0
- package/dist/billing.js +32 -0
- package/dist/billing.js.map +1 -0
- package/dist/budget.d.ts.map +1 -1
- package/dist/budget.js +18 -8
- package/dist/budget.js.map +1 -1
- package/dist/connector.d.ts +20 -1
- package/dist/connector.d.ts.map +1 -1
- package/dist/connector.js +42 -11
- package/dist/connector.js.map +1 -1
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +4 -2
- package/dist/conversation.js.map +1 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +5 -3
- package/dist/gateway.js.map +1 -1
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +7 -5
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/input-shape.d.ts.map +1 -1
- package/dist/input-shape.js +4 -2
- package/dist/input-shape.js.map +1 -1
- package/dist/ladder.d.ts.map +1 -1
- package/dist/ladder.js +2 -1
- package/dist/ladder.js.map +1 -1
- package/dist/levers.d.ts.map +1 -1
- package/dist/levers.js +5 -2
- package/dist/levers.js.map +1 -1
- package/dist/openai-usage.d.ts +18 -0
- package/dist/openai-usage.d.ts.map +1 -1
- package/dist/openai-usage.js +26 -1
- package/dist/openai-usage.js.map +1 -1
- package/dist/openrouter-activity.d.ts +6 -0
- package/dist/openrouter-activity.d.ts.map +1 -1
- package/dist/openrouter-activity.js +7 -0
- package/dist/openrouter-activity.js.map +1 -1
- package/dist/output-shape.d.ts.map +1 -1
- package/dist/output-shape.js +2 -1
- package/dist/output-shape.js.map +1 -1
- package/dist/pricing.d.ts +36 -4
- package/dist/pricing.d.ts.map +1 -1
- package/dist/pricing.js +804 -91
- package/dist/pricing.js.map +1 -1
- package/dist/receipt.d.ts +17 -0
- package/dist/receipt.d.ts.map +1 -1
- package/dist/receipt.js +10 -0
- package/dist/receipt.js.map +1 -1
- package/dist/repeats.d.ts.map +1 -1
- package/dist/repeats.js +4 -2
- package/dist/repeats.js.map +1 -1
- package/dist/reprice.d.ts +18 -2
- package/dist/reprice.d.ts.map +1 -1
- package/dist/reprice.js +34 -12
- package/dist/reprice.js.map +1 -1
- package/dist/rollup.d.ts.map +1 -1
- package/dist/rollup.js +3 -0
- package/dist/rollup.js.map +1 -1
- package/dist/savings.d.ts.map +1 -1
- package/dist/savings.js +10 -3
- package/dist/savings.js.map +1 -1
- package/dist/session-cost.d.ts.map +1 -1
- package/dist/session-cost.js +5 -2
- package/dist/session-cost.js.map +1 -1
- package/dist/session-ledger.d.ts.map +1 -1
- package/dist/session-ledger.js +12 -7
- package/dist/session-ledger.js.map +1 -1
- package/dist/truncation-retry.d.ts.map +1 -1
- package/dist/truncation-retry.js +4 -2
- package/dist/truncation-retry.js.map +1 -1
- package/dist/ttl-fit.d.ts +8 -0
- package/dist/ttl-fit.d.ts.map +1 -1
- package/dist/ttl-fit.js +25 -14
- package/dist/ttl-fit.js.map +1 -1
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +36 -1
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +91 -10
- package/dist/usage.js.map +1 -1
- package/package.json +1 -1
- package/src/advisories.ts +18 -10
- package/src/answer.ts +6 -1
- package/src/anthropic-usage.ts +7 -0
- package/src/billing.ts +37 -0
- package/src/budget.ts +18 -8
- package/src/connector.ts +58 -10
- package/src/conversation.ts +4 -2
- package/src/gateway.ts +5 -3
- package/src/guard.ts +7 -5
- package/src/index.ts +1 -1
- package/src/input-shape.ts +4 -2
- package/src/ladder.ts +2 -1
- package/src/levers.ts +5 -2
- package/src/openai-usage.ts +44 -2
- package/src/openrouter-activity.ts +13 -0
- package/src/output-shape.ts +2 -1
- package/src/pricing.ts +854 -96
- package/src/receipt.ts +23 -1
- package/src/repeats.ts +4 -2
- package/src/reprice.ts +57 -16
- package/src/rollup.ts +3 -0
- package/src/savings.ts +12 -5
- package/src/session-cost.ts +5 -2
- package/src/session-ledger.ts +18 -9
- package/src/truncation-retry.ts +4 -2
- package/src/ttl-fit.ts +39 -15
- package/src/types.ts +46 -0
- package/src/usage.ts +129 -11
package/src/usage.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
1
|
+
import { cacheTtlOf, effectivePricing, multipliersFor } from './pricing.js';
|
|
2
|
+
import { billedAt, promptSizeOf } from './billing.js';
|
|
2
3
|
import { createConversationTracker } from './conversation.js';
|
|
3
4
|
import { createOutputShapeTracker } from './output-shape.js';
|
|
4
5
|
import { createInputShapeTracker } from './input-shape.js';
|
|
@@ -159,6 +160,22 @@ export interface UsageRecord {
|
|
|
159
160
|
* must not congratulate a log that never measured.
|
|
160
161
|
*/
|
|
161
162
|
truncated: boolean | null;
|
|
163
|
+
/**
|
|
164
|
+
* How many requests this record sums, when it sums more than one.
|
|
165
|
+
*
|
|
166
|
+
* Absent on a record that is one call, which is every record a per-call log
|
|
167
|
+
* holds. A number above one, or `null` for "a sum, count not given", is a
|
|
168
|
+
* row out of a usage report: an Anthropic or OpenAI bucket, an OpenRouter
|
|
169
|
+
* day. It matters to a model priced by prompt length, because a request is
|
|
170
|
+
* priced on its own prompt and a row of many requests does not say any one
|
|
171
|
+
* of them. Such a row is priced at the dearer rate unless its whole prompt
|
|
172
|
+
* total is at or under the line, and the report says so.
|
|
173
|
+
*
|
|
174
|
+
* Read from `requests` when it is a positive integer or `null`; anything
|
|
175
|
+
* else is not this field, because a log that already uses the name for
|
|
176
|
+
* something of its own should not lose its records to it.
|
|
177
|
+
*/
|
|
178
|
+
requests?: number | null;
|
|
162
179
|
}
|
|
163
180
|
|
|
164
181
|
/** What a set of calls cost, split by where the money went. */
|
|
@@ -239,6 +256,21 @@ export interface UsageBreakdown {
|
|
|
239
256
|
truncatedOutputUsd: number;
|
|
240
257
|
/** Calls that recorded a stop reason at all, truncated or not. */
|
|
241
258
|
stopReasonCalls: number;
|
|
259
|
+
/**
|
|
260
|
+
* Records billed at the dearer of two rates because which one applied could
|
|
261
|
+
* not be decided, and what they cost each way.
|
|
262
|
+
*
|
|
263
|
+
* A model priced by prompt length bills each request on its own prompt, and
|
|
264
|
+
* a row that sums requests does not say any one prompt's length. Those rows
|
|
265
|
+
* are billed at the dearer rate, so `totalUsd` is a ceiling on them and never
|
|
266
|
+
* a figure that flatters; `tierCeilingUsd` is what they contribute to it and
|
|
267
|
+
* `tierFloorUsd` what the same tokens come to at the cheaper rate. The true
|
|
268
|
+
* bill for them is between the two, and a report that printed only the
|
|
269
|
+
* ceiling would be right and useless.
|
|
270
|
+
*/
|
|
271
|
+
tierCeilingCalls: number;
|
|
272
|
+
tierCeilingUsd: number;
|
|
273
|
+
tierFloorUsd: number;
|
|
242
274
|
}
|
|
243
275
|
|
|
244
276
|
/**
|
|
@@ -597,6 +629,9 @@ const EMPTY = (): UsageBreakdown => ({
|
|
|
597
629
|
truncatedCalls: 0,
|
|
598
630
|
truncatedOutputUsd: 0,
|
|
599
631
|
stopReasonCalls: 0,
|
|
632
|
+
tierCeilingCalls: 0,
|
|
633
|
+
tierCeilingUsd: 0,
|
|
634
|
+
tierFloorUsd: 0,
|
|
600
635
|
});
|
|
601
636
|
|
|
602
637
|
/**
|
|
@@ -713,12 +748,24 @@ export function parseUsageLine(line: string): UsageRecord | null {
|
|
|
713
748
|
* in `prompt_tokens`**, while Anthropic reports them separately and does not.
|
|
714
749
|
* Subtracting in one case and not the other is the difference between a correct
|
|
715
750
|
* bill and one that charges the cached half twice.
|
|
751
|
+
*
|
|
752
|
+
* **And the Responses API spells it `input_tokens_details`**, under an
|
|
753
|
+
* `input_tokens` that likewise includes the cached tokens. Only the Chat
|
|
754
|
+
* Completions spelling was read, so a Responses log, OpenAI's default API,
|
|
755
|
+
* billed every cached token at the full input rate: a call that read 90,000
|
|
756
|
+
* of its 100,000 tokens from the cache came out four times its price, and the
|
|
757
|
+
* cache verdict said caching was never used.
|
|
758
|
+
*
|
|
759
|
+
* GPT-5.6 and later also report `cache_write_tokens` in the same object,
|
|
760
|
+
* inside the same total, and bill them at 1.25x input. They are subtracted
|
|
761
|
+
* with the cached tokens and land in the write bucket, whose TTL the model
|
|
762
|
+
* states (`cacheTtl`): one kind of entry, so nothing about it is assumed.
|
|
716
763
|
*/
|
|
764
|
+
const detailsRaw = usage.prompt_tokens_details ?? usage.input_tokens_details;
|
|
717
765
|
const details =
|
|
718
|
-
typeof
|
|
719
|
-
? (usage.prompt_tokens_details as Record<string, unknown>)
|
|
720
|
-
: null;
|
|
766
|
+
typeof detailsRaw === 'object' && detailsRaw !== null ? (detailsRaw as Record<string, unknown>) : null;
|
|
721
767
|
const openAiCached = details ? readCount(details.cached_tokens) : ({ kind: 'absent' } as Count);
|
|
768
|
+
const openAiWrite = details ? readCount(details.cache_write_tokens) : ({ kind: 'absent' } as Count);
|
|
722
769
|
|
|
723
770
|
/**
|
|
724
771
|
* Anthropic splits cache writes by time-to-live, and the two cost different
|
|
@@ -769,6 +816,7 @@ export function parseUsageLine(line: string): UsageRecord | null {
|
|
|
769
816
|
cacheRead: readCount(usage.cache_read_input_tokens, usage.cacheReadTokens),
|
|
770
817
|
cacheWrite: readCount(usage.cache_creation_input_tokens, usage.cacheWriteTokens),
|
|
771
818
|
openAiCached,
|
|
819
|
+
openAiWrite,
|
|
772
820
|
geminiCached,
|
|
773
821
|
write5m,
|
|
774
822
|
write1h,
|
|
@@ -791,6 +839,8 @@ export function parseUsageLine(line: string): UsageRecord | null {
|
|
|
791
839
|
// Both providers that fold cached tokens into the prompt count, one
|
|
792
840
|
// subtraction. They cannot coexist on a record: the shapes share no field.
|
|
793
841
|
const cached = valueOf(counts.openAiCached!) + valueOf(counts.geminiCached!);
|
|
842
|
+
// Inside the OpenAI total like the cached tokens, and taken out of it the same way.
|
|
843
|
+
const openAiWritten = valueOf(counts.openAiWrite!);
|
|
794
844
|
const flatWrite = valueOf(counts.cacheWrite!);
|
|
795
845
|
const split5m = valueOf(counts.write5m!);
|
|
796
846
|
const split1h = valueOf(counts.write1h!);
|
|
@@ -798,16 +848,17 @@ export function parseUsageLine(line: string): UsageRecord | null {
|
|
|
798
848
|
|
|
799
849
|
return {
|
|
800
850
|
model,
|
|
801
|
-
inputTokens: Math.max(0, valueOf(counts.input!) - cached),
|
|
851
|
+
inputTokens: Math.max(0, valueOf(counts.input!) - cached - openAiWritten),
|
|
802
852
|
cacheReadTokens: counts.cacheRead!.kind === 'ok' ? counts.cacheRead!.value : cached,
|
|
803
853
|
/**
|
|
804
854
|
* The split when the log carries it, the flat number otherwise — and
|
|
805
855
|
* `writeTtlKnown` says which, so the report can admit that a rate was assumed
|
|
806
856
|
* rather than quietly choosing the cheaper one.
|
|
807
857
|
*/
|
|
808
|
-
cacheWrite5mTokens: hasSplit ? split5m : flatWrite,
|
|
858
|
+
cacheWrite5mTokens: (hasSplit ? split5m : flatWrite) + openAiWritten,
|
|
809
859
|
cacheWrite1hTokens: hasSplit ? split1h : 0,
|
|
810
|
-
|
|
860
|
+
// `cache_write_tokens` names no TTL either; see the note in `profileUsage`.
|
|
861
|
+
writeTtlKnown: hasSplit || (flatWrite === 0 && openAiWritten === 0),
|
|
811
862
|
outputTokens: valueOf(counts.output!),
|
|
812
863
|
/**
|
|
813
864
|
* Trimmed and internally normalised: any whitespace run becomes one space.
|
|
@@ -846,9 +897,22 @@ export function parseUsageLine(line: string): UsageRecord | null {
|
|
|
846
897
|
if (typeof reason !== 'string') return null;
|
|
847
898
|
return reason === 'max_tokens' || reason === 'length' || reason === 'MAX_TOKENS';
|
|
848
899
|
})(),
|
|
900
|
+
...requestsOf(record.requests),
|
|
849
901
|
};
|
|
850
902
|
}
|
|
851
903
|
|
|
904
|
+
/**
|
|
905
|
+
* `requests`, when it is this field: a positive integer or `null`.
|
|
906
|
+
*
|
|
907
|
+
* One is the same as absent, so it is left absent and a per-call record stays
|
|
908
|
+
* the shape it always was.
|
|
909
|
+
*/
|
|
910
|
+
function requestsOf(value: unknown): { requests?: number | null } {
|
|
911
|
+
if (value === null) return { requests: null };
|
|
912
|
+
if (typeof value === 'number' && Number.isInteger(value) && value > 1) return { requests: value };
|
|
913
|
+
return {};
|
|
914
|
+
}
|
|
915
|
+
|
|
852
916
|
/**
|
|
853
917
|
* One slice's outcome tally, in the same shape as the whole log's.
|
|
854
918
|
*
|
|
@@ -944,9 +1008,40 @@ function add(into: UsageBreakdown, record: UsageRecord, catalogue: PricingCatalo
|
|
|
944
1008
|
if (!model) return false;
|
|
945
1009
|
|
|
946
1010
|
countInto(into, record);
|
|
947
|
-
|
|
948
|
-
|
|
1011
|
+
/**
|
|
1012
|
+
* **Each call at the price of its own moment.** A bill is history: a call
|
|
1013
|
+
* made inside a promotion was billed at the promotional rate, a DeepSeek
|
|
1014
|
+
* call at 03:00 UTC on a weekday was billed at peak, and a call before a
|
|
1015
|
+
* published price change was billed at the old price. Priced at the report's
|
|
1016
|
+
* date instead, every one of those came out at whatever applied the day the
|
|
1017
|
+
* report ran: a clock-priced log read during peak hours was all peak, and an
|
|
1018
|
+
* August log read in October lost its August promotion. `on` is the date for
|
|
1019
|
+
* a record that carries no clock, which is the only thing it can mean here.
|
|
1020
|
+
*/
|
|
1021
|
+
const at = billedAt(record, on);
|
|
1022
|
+
/**
|
|
1023
|
+
* **Each request on its own prompt.** A model priced by prompt length counts
|
|
1024
|
+
* every input token a request carried, cache reads and writes included, and
|
|
1025
|
+
* a record that is one call says exactly that. A record that sums requests
|
|
1026
|
+
* gives only a bound: no request in it was larger than the whole row.
|
|
1027
|
+
*/
|
|
1028
|
+
const priced = effectivePricing(model, at, promptSizeOf(record));
|
|
1029
|
+
const { inputPerMTok, outputPerMTok } = priced;
|
|
1030
|
+
// Dated, like the rate above: a repriced cache read is part of the reprice.
|
|
1031
|
+
const rates = multipliersFor(model, at);
|
|
949
1032
|
const per = (tokens: number, rate: number): number => (tokens / 1_000_000) * rate;
|
|
1033
|
+
const recordUsd = (input: number, output: number): number =>
|
|
1034
|
+
per(record.inputTokens, input) +
|
|
1035
|
+
per(record.cacheReadTokens, input * rates.cacheRead) +
|
|
1036
|
+
per(record.cacheWrite5mTokens, input * rates.cacheWrite5m) +
|
|
1037
|
+
per(record.cacheWrite1hTokens, input * rates.cacheWrite1h) +
|
|
1038
|
+
per(record.outputTokens, output);
|
|
1039
|
+
const floor = priced.tier?.decided === false ? priced.tier.floor : undefined;
|
|
1040
|
+
if (floor !== undefined) {
|
|
1041
|
+
into.tierCeilingCalls += 1;
|
|
1042
|
+
into.tierCeilingUsd += recordUsd(inputPerMTok, outputPerMTok);
|
|
1043
|
+
into.tierFloorUsd += recordUsd(floor.inputPerMTok, floor.outputPerMTok);
|
|
1044
|
+
}
|
|
950
1045
|
|
|
951
1046
|
into.inputUsd += per(record.inputTokens, inputPerMTok);
|
|
952
1047
|
into.cacheReadUsd += per(record.cacheReadTokens, inputPerMTok * rates.cacheRead);
|
|
@@ -991,7 +1086,11 @@ function add(into: UsageBreakdown, record: UsageRecord, catalogue: PricingCatalo
|
|
|
991
1086
|
|
|
992
1087
|
export interface UsageProfileOptions {
|
|
993
1088
|
catalogue: PricingCatalogue;
|
|
994
|
-
/**
|
|
1089
|
+
/**
|
|
1090
|
+
* The date a record with no timestamp is priced at, and the date the
|
|
1091
|
+
* forward-looking findings (levers, what-ifs) are priced at. A record that
|
|
1092
|
+
* carries its own `ts` is billed at its own moment: see `add`.
|
|
1093
|
+
*/
|
|
995
1094
|
on?: Date;
|
|
996
1095
|
/**
|
|
997
1096
|
* Profile only the records carrying this label — the drill-down, once the
|
|
@@ -1089,11 +1188,30 @@ export function profileUsage(text: string, options: UsageProfileOptions): UsageP
|
|
|
1089
1188
|
const line = lines[i]!.trim();
|
|
1090
1189
|
if (line === '') continue;
|
|
1091
1190
|
|
|
1092
|
-
|
|
1191
|
+
let record = parseUsageLine(line);
|
|
1093
1192
|
if (!record) {
|
|
1094
1193
|
skippedLines.push(i + 1);
|
|
1095
1194
|
continue;
|
|
1096
1195
|
}
|
|
1196
|
+
/**
|
|
1197
|
+
* A write whose TTL the line did not state is an assumption only where
|
|
1198
|
+
* there was a choice. `cache_write_tokens` carries no TTL: on Kimi K3,
|
|
1199
|
+
* which sells a 5-minute and a 1-hour entry, the cheaper is assumed and
|
|
1200
|
+
* the report says so; on OpenAI's GPT-5.6 and later, which sell one, and
|
|
1201
|
+
* on its earlier models, whose writes cost what input costs whatever they
|
|
1202
|
+
* last, there is nothing to assume and nothing is said.
|
|
1203
|
+
*/
|
|
1204
|
+
if (!record.writeTtlKnown) {
|
|
1205
|
+
const priced = catalogue.byId.get(record.model);
|
|
1206
|
+
// One kind of entry, or two at one price: an assumption that cannot move the bill is not one.
|
|
1207
|
+
if (
|
|
1208
|
+
priced !== undefined
|
|
1209
|
+
&& (cacheTtlOf(priced).longerMs === null
|
|
1210
|
+
|| multipliersFor(priced).cacheWrite1h === multipliersFor(priced).cacheWrite5m)
|
|
1211
|
+
) {
|
|
1212
|
+
record = { ...record, writeTtlKnown: true };
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1097
1215
|
|
|
1098
1216
|
// The drill-down: after the skip accounting, so a corrupt line is reported
|
|
1099
1217
|
// whatever it might have been labelled.
|