@trazum/core 2.4.3 → 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 +35 -3
- package/dist/pricing.d.ts.map +1 -1
- package/dist/pricing.js +569 -54
- 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 +24 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +31 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +79 -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 +614 -59
- 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 +18 -0
- package/src/usage.ts +112 -10
package/src/receipt.ts
CHANGED
|
@@ -210,7 +210,18 @@ export type ReceiptGap =
|
|
|
210
210
|
* and a document that says `counting: 'counted'` while quietly resting on an
|
|
211
211
|
* assumption is claiming a precision it does not have.
|
|
212
212
|
*/
|
|
213
|
-
| { kind: 'assumed-write-ttl'; calls: number }
|
|
213
|
+
| { kind: 'assumed-write-ttl'; calls: number }
|
|
214
|
+
/**
|
|
215
|
+
* Rows billed at the dearer of two rates because which one applied could
|
|
216
|
+
* not be decided: a model priced by prompt length, and a row that sums
|
|
217
|
+
* requests without saying any one prompt's length.
|
|
218
|
+
*
|
|
219
|
+
* Reported because it changes what kind of figure this is, in the other
|
|
220
|
+
* direction from `assumed-write-ttl`: the total is a **ceiling** on these
|
|
221
|
+
* rows. `ceilingUsd` is their share of it, `floorUsd` the same tokens at the
|
|
222
|
+
* cheaper rate; the true figure for them is between the two.
|
|
223
|
+
*/
|
|
224
|
+
| { kind: 'tier-ceiling'; calls: number; models: string[]; ceilingUsd: number; floorUsd: number };
|
|
214
225
|
|
|
215
226
|
export interface ReceiptDocument {
|
|
216
227
|
/** The one thing a consumer must branch on. Set here, by the builder. */
|
|
@@ -389,6 +400,17 @@ export function receiptFrom(
|
|
|
389
400
|
);
|
|
390
401
|
if (assumed > 0) gaps.push({ kind: 'assumed-write-ttl', calls: assumed });
|
|
391
402
|
|
|
403
|
+
const ceilinged = report.byModel.filter((row) => row.breakdown.tierCeilingCalls > 0);
|
|
404
|
+
if (ceilinged.length > 0) {
|
|
405
|
+
gaps.push({
|
|
406
|
+
kind: 'tier-ceiling',
|
|
407
|
+
calls: report.total.tierCeilingCalls,
|
|
408
|
+
models: ceilinged.map((row) => row.model),
|
|
409
|
+
ceilingUsd: report.total.tierCeilingUsd,
|
|
410
|
+
floorUsd: report.total.tierFloorUsd,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
392
414
|
return {
|
|
393
415
|
schemaVersion: 1,
|
|
394
416
|
emittedAt: options.emittedAt?.toISOString() ?? null,
|
package/src/repeats.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
2
|
+
import { billedAt, promptSizeOf } from './billing.js';
|
|
2
3
|
import { UNLABELLED } from './usage.js';
|
|
3
4
|
import type { PricingCatalogue } from './pricing.js';
|
|
4
5
|
import type { UsageRecord } from './usage.js';
|
|
@@ -119,8 +120,9 @@ export function createRepeatsTracker(options: RepeatsOptions): RepeatsTracker {
|
|
|
119
120
|
if (gap < 0 || gap >= withinMs) return;
|
|
120
121
|
if (before.tokens !== tokens) return;
|
|
121
122
|
|
|
122
|
-
const
|
|
123
|
-
const
|
|
123
|
+
const at = billedAt(record, on);
|
|
124
|
+
const { inputPerMTok, outputPerMTok } = effectivePricing(model, at, promptSizeOf(record));
|
|
125
|
+
const rates = multipliersFor(model, at);
|
|
124
126
|
const per = (count: number, rate: number): number => (count / 1_000_000) * rate;
|
|
125
127
|
slice.repeats += 1;
|
|
126
128
|
slice.usd +=
|
package/src/reprice.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
2
|
-
import type { PricingCatalogue } from './pricing.js';
|
|
2
|
+
import type { PricingCatalogue, PromptSize } from './pricing.js';
|
|
3
3
|
import type { ModelPricing } from './types.js';
|
|
4
4
|
import type { ReceiptDocument } from './receipt.js';
|
|
5
5
|
import type { UsageBreakdown, UsageProfileReport } from './usage.js';
|
|
@@ -158,6 +158,19 @@ export interface RepriceReport {
|
|
|
158
158
|
* is the reader's.
|
|
159
159
|
*/
|
|
160
160
|
batchOnTarget: { targetUsd: number } | null;
|
|
161
|
+
/**
|
|
162
|
+
* Slices priced at the target's dearer band because which band applied
|
|
163
|
+
* could not be decided, and what `targetUsd` would be with them at the
|
|
164
|
+
* cheaper one. `null` when every slice was decided.
|
|
165
|
+
*
|
|
166
|
+
* A target priced by prompt length bills each request on its own prompt; a
|
|
167
|
+
* slice is a sum, and its largest call is all it says about any one prompt.
|
|
168
|
+
* A slice whose largest call is under the line is under it throughout, and
|
|
169
|
+
* decided. One whose largest call is over is not, and is priced at the
|
|
170
|
+
* dearer band, so the comparison is a ceiling and never a saving it cannot
|
|
171
|
+
* stand behind; this is the other end.
|
|
172
|
+
*/
|
|
173
|
+
tierCeiling: { slices: number; targetFloorUsd: number } | null;
|
|
161
174
|
/** Always true. A field, not a comment, so a rendering can print it. */
|
|
162
175
|
sameTokensAssumed: true;
|
|
163
176
|
}
|
|
@@ -177,17 +190,35 @@ export function priceTokensOn(
|
|
|
177
190
|
>,
|
|
178
191
|
model: ModelPricing,
|
|
179
192
|
on: Date = new Date(),
|
|
193
|
+
size: PromptSize = {},
|
|
180
194
|
): number {
|
|
181
|
-
|
|
182
|
-
|
|
195
|
+
return priceTokensRanged(breakdown, model, on, size).usd;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The same price, and the cheaper one when a size band could not be decided. */
|
|
199
|
+
function priceTokensRanged(
|
|
200
|
+
breakdown: Pick<
|
|
201
|
+
UsageBreakdown,
|
|
202
|
+
'inputTokens' | 'cacheReadTokens' | 'cacheWrite5mTokens' | 'cacheWrite1hTokens' | 'outputTokens'
|
|
203
|
+
>,
|
|
204
|
+
model: ModelPricing,
|
|
205
|
+
on: Date,
|
|
206
|
+
size: PromptSize,
|
|
207
|
+
): { usd: number; floorUsd: number | null } {
|
|
208
|
+
const priced = effectivePricing(model, on, size);
|
|
209
|
+
const rates = multipliersFor(model, on);
|
|
183
210
|
const per = (tokens: number, rate: number): number => (tokens / 1_000_000) * rate;
|
|
184
|
-
|
|
211
|
+
const at = (inputPerMTok: number, outputPerMTok: number): number =>
|
|
185
212
|
per(breakdown.inputTokens, inputPerMTok) +
|
|
186
213
|
per(breakdown.cacheReadTokens, inputPerMTok * rates.cacheRead) +
|
|
187
214
|
per(breakdown.cacheWrite5mTokens, inputPerMTok * rates.cacheWrite5m) +
|
|
188
215
|
per(breakdown.cacheWrite1hTokens, inputPerMTok * rates.cacheWrite1h) +
|
|
189
|
-
per(breakdown.outputTokens, outputPerMTok)
|
|
190
|
-
|
|
216
|
+
per(breakdown.outputTokens, outputPerMTok);
|
|
217
|
+
const floor = priced.tier?.decided === false ? priced.tier.floor : undefined;
|
|
218
|
+
return {
|
|
219
|
+
usd: at(priced.inputPerMTok, priced.outputPerMTok),
|
|
220
|
+
floorUsd: floor === undefined ? null : at(floor.inputPerMTok, floor.outputPerMTok),
|
|
221
|
+
};
|
|
191
222
|
}
|
|
192
223
|
|
|
193
224
|
/**
|
|
@@ -249,6 +280,8 @@ function repriceSlices(
|
|
|
249
280
|
const overContext: OverContextSlice[] = [];
|
|
250
281
|
const alreadyOnTarget = { calls: 0, usd: 0 };
|
|
251
282
|
let assumedWriteTtlCalls = 0;
|
|
283
|
+
let ceilingSlices = 0;
|
|
284
|
+
let floorSaving = 0;
|
|
252
285
|
|
|
253
286
|
for (const breakdown of input.slices) {
|
|
254
287
|
if (breakdown.model === target.id) {
|
|
@@ -266,7 +299,14 @@ function repriceSlices(
|
|
|
266
299
|
});
|
|
267
300
|
continue;
|
|
268
301
|
}
|
|
269
|
-
|
|
302
|
+
/* The slice's largest call bounds every prompt in it. */
|
|
303
|
+
const bound: PromptSize = { inputTokensAtMost: breakdown.maxCallInputTokens };
|
|
304
|
+
const ranged = priceTokensRanged(breakdown, target, on, bound);
|
|
305
|
+
const targetUsd = ranged.usd;
|
|
306
|
+
if (ranged.floorUsd !== null) {
|
|
307
|
+
ceilingSlices += 1;
|
|
308
|
+
floorSaving += ranged.usd - ranged.floorUsd;
|
|
309
|
+
}
|
|
270
310
|
assumedWriteTtlCalls += breakdown.assumedWriteTtlCalls;
|
|
271
311
|
/**
|
|
272
312
|
* Cache traffic the target could not grant. The comparison is against the
|
|
@@ -297,6 +337,7 @@ function repriceSlices(
|
|
|
297
337
|
},
|
|
298
338
|
target,
|
|
299
339
|
on,
|
|
340
|
+
bound,
|
|
300
341
|
),
|
|
301
342
|
}
|
|
302
343
|
: null;
|
|
@@ -323,20 +364,19 @@ function repriceSlices(
|
|
|
323
364
|
* aggregates rather than by discounting `targetUsd`, because the discount
|
|
324
365
|
* covers input and output while cache traffic rides through unchanged.
|
|
325
366
|
*/
|
|
326
|
-
const batchRate = multipliersFor(target).batch;
|
|
367
|
+
const batchRate = multipliersFor(target, on).batch;
|
|
327
368
|
let batchOnTarget: { targetUsd: number } | null = null;
|
|
328
369
|
if (batchRate !== null && batchRate < 1 && slices.length > 0) {
|
|
329
|
-
const { inputPerMTok, outputPerMTok } = effectivePricing(target, on);
|
|
330
370
|
const movable = input.slices.filter(
|
|
331
371
|
(slice) => slice.model !== target.id && slice.maxCallInputTokens <= target.contextWindow,
|
|
332
372
|
);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
);
|
|
373
|
+
// Each slice at the band its own pricing above used, so the two cannot disagree.
|
|
374
|
+
const inOutUsd = movable.reduce((sum, slice) => {
|
|
375
|
+
const { inputPerMTok, outputPerMTok } = effectivePricing(target, on, {
|
|
376
|
+
inputTokensAtMost: slice.maxCallInputTokens,
|
|
377
|
+
});
|
|
378
|
+
return sum + (slice.inputTokens / 1_000_000) * inputPerMTok + (slice.outputTokens / 1_000_000) * outputPerMTok;
|
|
379
|
+
}, 0);
|
|
340
380
|
batchOnTarget = { targetUsd: targetUsd - inOutUsd * (1 - batchRate) };
|
|
341
381
|
}
|
|
342
382
|
|
|
@@ -356,6 +396,7 @@ function repriceSlices(
|
|
|
356
396
|
unpricedModels: [...input.unpricedModels],
|
|
357
397
|
unpricedCalls: input.unpricedCalls,
|
|
358
398
|
batchOnTarget,
|
|
399
|
+
tierCeiling: ceilingSlices === 0 ? null : { slices: ceilingSlices, targetFloorUsd: targetUsd - floorSaving },
|
|
359
400
|
sameTokensAssumed: true,
|
|
360
401
|
};
|
|
361
402
|
}
|
package/src/rollup.ts
CHANGED
|
@@ -316,6 +316,9 @@ const BREAKDOWN_SUM = [
|
|
|
316
316
|
'truncatedCalls',
|
|
317
317
|
'truncatedOutputUsd',
|
|
318
318
|
'stopReasonCalls',
|
|
319
|
+
'tierCeilingCalls',
|
|
320
|
+
'tierCeilingUsd',
|
|
321
|
+
'tierFloorUsd',
|
|
319
322
|
] as const;
|
|
320
323
|
|
|
321
324
|
/** Fields whose combination is a maximum, because a sum would invent a call. */
|
package/src/savings.ts
CHANGED
|
@@ -53,7 +53,14 @@ export function computeSavings(
|
|
|
53
53
|
pricing: PricingCatalogue = BUNDLED_CATALOGUE,
|
|
54
54
|
): SavingsReport {
|
|
55
55
|
const model = modelFrom(pricing, usage.model);
|
|
56
|
-
|
|
56
|
+
/*
|
|
57
|
+
* Each prompt at its own length. On a model priced by prompt length the two
|
|
58
|
+
* sides can sit in different bands, and an edit that brings a prompt under
|
|
59
|
+
* the line saves the whole difference in rate, not only the tokens cut.
|
|
60
|
+
*/
|
|
61
|
+
const ratesBefore = effectivePricing(model, on, { inputTokens: tokensBefore });
|
|
62
|
+
const ratesAfter = effectivePricing(model, on, { inputTokens: tokensAfter });
|
|
63
|
+
const promoApplied = ratesBefore.promoApplied || ratesAfter.promoApplied;
|
|
57
64
|
// A provider with no batch API gets no discount even when the caller ticked
|
|
58
65
|
// the box: `batchEligible` describes the work, not what the provider sells.
|
|
59
66
|
const batchDiscount = multipliersFor(model).batch ?? 1;
|
|
@@ -61,16 +68,16 @@ export function computeSavings(
|
|
|
61
68
|
const before = costOfCall(
|
|
62
69
|
tokensBefore,
|
|
63
70
|
usage.avgOutputTokens,
|
|
64
|
-
inputPerMTok,
|
|
65
|
-
outputPerMTok,
|
|
71
|
+
ratesBefore.inputPerMTok,
|
|
72
|
+
ratesBefore.outputPerMTok,
|
|
66
73
|
usage.batchEligible,
|
|
67
74
|
batchDiscount,
|
|
68
75
|
);
|
|
69
76
|
const after = costOfCall(
|
|
70
77
|
tokensAfter,
|
|
71
78
|
usage.avgOutputTokens,
|
|
72
|
-
inputPerMTok,
|
|
73
|
-
outputPerMTok,
|
|
79
|
+
ratesAfter.inputPerMTok,
|
|
80
|
+
ratesAfter.outputPerMTok,
|
|
74
81
|
usage.batchEligible,
|
|
75
82
|
batchDiscount,
|
|
76
83
|
);
|
package/src/session-cost.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
2
|
+
import { billedAt, promptSizeOf } from './billing.js';
|
|
2
3
|
import { UNLABELLED } from './usage.js';
|
|
3
4
|
import type { PricingCatalogue } from './pricing.js';
|
|
4
5
|
import type { UsageRecord } from './usage.js';
|
|
@@ -75,8 +76,10 @@ export interface SessionCostTracker {
|
|
|
75
76
|
export function costOf(record: UsageRecord, catalogue: PricingCatalogue, on: Date): number | null {
|
|
76
77
|
const model = catalogue.byId.get(record.model);
|
|
77
78
|
if (!model) return null;
|
|
78
|
-
|
|
79
|
-
const
|
|
79
|
+
// At the call's own moment and on its own prompt, as the bill prices it.
|
|
80
|
+
const at = billedAt(record, on);
|
|
81
|
+
const { inputPerMTok, outputPerMTok } = effectivePricing(model, at, promptSizeOf(record));
|
|
82
|
+
const rates = multipliersFor(model, at);
|
|
80
83
|
const per = (tokens: number, rate: number): number => (tokens / 1_000_000) * rate;
|
|
81
84
|
return (
|
|
82
85
|
per(record.inputTokens, inputPerMTok) +
|
package/src/session-ledger.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
2
|
+
import { billedAt, promptSizeOf } from './billing.js';
|
|
2
3
|
import { UNLABELLED } from './usage.js';
|
|
3
4
|
import type { PricingCatalogue } from './pricing.js';
|
|
4
5
|
import type { UsageRecord } from './usage.js';
|
|
@@ -77,6 +78,12 @@ interface SessionTally {
|
|
|
77
78
|
write5mTokens: number;
|
|
78
79
|
write1hTokens: number;
|
|
79
80
|
assumedTokens: number;
|
|
81
|
+
/**
|
|
82
|
+
* What this conversation's writes cost, each call at its own moment and on
|
|
83
|
+
* its own prompt, so a one-turn conversation is priced exactly as the bill
|
|
84
|
+
* priced its one call.
|
|
85
|
+
*/
|
|
86
|
+
writeUsd: number;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
const median = (sorted: number[]): number => {
|
|
@@ -107,10 +114,18 @@ export function createSessionLedgerTracker(options: SessionLedgerOptions): Sessi
|
|
|
107
114
|
}
|
|
108
115
|
let tally = sessions.get(record.session);
|
|
109
116
|
if (!tally) {
|
|
110
|
-
tally = { turns: 0, write5mTokens: 0, write1hTokens: 0, assumedTokens: 0 };
|
|
117
|
+
tally = { turns: 0, write5mTokens: 0, write1hTokens: 0, assumedTokens: 0, writeUsd: 0 };
|
|
111
118
|
sessions.set(record.session, tally);
|
|
112
119
|
}
|
|
113
120
|
tally.turns += 1;
|
|
121
|
+
const model = catalogue.byId.get(record.model)!;
|
|
122
|
+
const at = billedAt(record, on);
|
|
123
|
+
const { inputPerMTok } = effectivePricing(model, at, promptSizeOf(record));
|
|
124
|
+
const rates = multipliersFor(model, at);
|
|
125
|
+
// The bill's own convention: unstated TTLs sit in the 5m bucket at the cheaper rate.
|
|
126
|
+
tally.writeUsd +=
|
|
127
|
+
((record.cacheWrite5mTokens * rates.cacheWrite5m + record.cacheWrite1hTokens * rates.cacheWrite1h) / 1_000_000)
|
|
128
|
+
* inputPerMTok;
|
|
114
129
|
if (record.writeTtlKnown) {
|
|
115
130
|
tally.write5mTokens += record.cacheWrite5mTokens;
|
|
116
131
|
tally.write1hTokens += record.cacheWrite1hTokens;
|
|
@@ -129,6 +144,7 @@ export function createSessionLedgerTracker(options: SessionLedgerOptions): Sessi
|
|
|
129
144
|
let write5m = 0;
|
|
130
145
|
let write1h = 0;
|
|
131
146
|
let assumed = 0;
|
|
147
|
+
let singleTurnWriteUsd = 0;
|
|
132
148
|
const turnCounts: number[] = [];
|
|
133
149
|
for (const tally of sessions.values()) {
|
|
134
150
|
turnCounts.push(tally.turns);
|
|
@@ -137,6 +153,7 @@ export function createSessionLedgerTracker(options: SessionLedgerOptions): Sessi
|
|
|
137
153
|
write5m += tally.write5mTokens;
|
|
138
154
|
write1h += tally.write1hTokens;
|
|
139
155
|
assumed += tally.assumedTokens;
|
|
156
|
+
singleTurnWriteUsd += tally.writeUsd;
|
|
140
157
|
}
|
|
141
158
|
const singleTurnWriteTokens = write5m + write1h + assumed;
|
|
142
159
|
// One-turn conversations that wrote nothing wasted nothing; a row about
|
|
@@ -146,14 +163,6 @@ export function createSessionLedgerTracker(options: SessionLedgerOptions): Sessi
|
|
|
146
163
|
const split = key.indexOf('\n');
|
|
147
164
|
const modelId = key.slice(split + 1);
|
|
148
165
|
const model = catalogue.byId.get(modelId)!;
|
|
149
|
-
const { inputPerMTok } = effectivePricing(model, on);
|
|
150
|
-
const rates = multipliersFor(model);
|
|
151
|
-
const per = (tokens: number, rate: number): number =>
|
|
152
|
-
(tokens / 1_000_000) * inputPerMTok * rate;
|
|
153
|
-
// The bill's own convention: unstated TTLs at the cheaper rate, so this
|
|
154
|
-
// is the same floor the totals already stand on — never a new guess.
|
|
155
|
-
const singleTurnWriteUsd =
|
|
156
|
-
per(write5m + assumed, rates.cacheWrite5m) + per(write1h, rates.cacheWrite1h);
|
|
157
166
|
|
|
158
167
|
turnCounts.sort((a, b) => a - b);
|
|
159
168
|
out.push({
|
package/src/truncation-retry.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
2
|
+
import { billedAt, promptSizeOf } from './billing.js';
|
|
2
3
|
import { UNLABELLED } from './usage.js';
|
|
3
4
|
import type { PricingCatalogue } from './pricing.js';
|
|
4
5
|
import type { UsageRecord } from './usage.js';
|
|
@@ -90,8 +91,9 @@ export function createTruncationRetryTracker(options: TruncationRetryOptions): T
|
|
|
90
91
|
const priceOf = (record: UsageRecord): number | null => {
|
|
91
92
|
const model = catalogue.byId.get(record.model);
|
|
92
93
|
if (!model) return null;
|
|
93
|
-
const
|
|
94
|
-
const
|
|
94
|
+
const at = billedAt(record, on);
|
|
95
|
+
const { inputPerMTok, outputPerMTok } = effectivePricing(model, at, promptSizeOf(record));
|
|
96
|
+
const rates = multipliersFor(model, at);
|
|
95
97
|
const per = (count: number, rate: number): number => (count / 1_000_000) * rate;
|
|
96
98
|
return (
|
|
97
99
|
per(record.inputTokens, inputPerMTok) +
|
package/src/ttl-fit.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 { UNLABELLED } from './usage.js';
|
|
3
4
|
import type { PricingCatalogue } from './pricing.js';
|
|
4
5
|
import type { UsageRecord } from './usage.js';
|
|
@@ -79,6 +80,14 @@ export interface CacheTtlFit {
|
|
|
79
80
|
/** Write tokens whose TTL the log did not record. */
|
|
80
81
|
assumedTtlTokens: number;
|
|
81
82
|
verdict: TtlFitVerdict;
|
|
83
|
+
/**
|
|
84
|
+
* How long this model's entries live: the one a write buys, and the longer
|
|
85
|
+
* one the provider also sells, or `null` when it sells none. Five minutes
|
|
86
|
+
* and an hour on Anthropic; thirty minutes and nothing longer on OpenAI's
|
|
87
|
+
* GPT-5.6 and later. A verdict reads them, and so does the sentence about it.
|
|
88
|
+
*/
|
|
89
|
+
entryTtlMs: number;
|
|
90
|
+
longerTtlMs: number | null;
|
|
82
91
|
/**
|
|
83
92
|
* What the 1-hour writes would save at the 5-minute rate, when the gaps show
|
|
84
93
|
* the hour is never needed. Exact — the same tokens at 1.25x instead of 2x,
|
|
@@ -104,6 +113,12 @@ interface Slice {
|
|
|
104
113
|
write5mTokens: number;
|
|
105
114
|
write1hTokens: number;
|
|
106
115
|
assumedTtlTokens: number;
|
|
116
|
+
/**
|
|
117
|
+
* What the 1-hour writes would have saved at the 5-minute rate, each call at
|
|
118
|
+
* its own moment and on its own prompt: one rate for the whole slice priced
|
|
119
|
+
* a call on another band, or in another price period, at the wrong figure.
|
|
120
|
+
*/
|
|
121
|
+
overlongUsd: number;
|
|
107
122
|
/** Timestamps per session, sorted only at the end. */
|
|
108
123
|
sessions: Map<string, number[]>;
|
|
109
124
|
}
|
|
@@ -138,13 +153,21 @@ export function createTtlFitTracker(options: TtlFitOptions): TtlFitTracker {
|
|
|
138
153
|
const key = `${record.label ?? UNLABELLED}\n${record.model}`;
|
|
139
154
|
let slice = slices.get(key);
|
|
140
155
|
if (!slice) {
|
|
141
|
-
slice = { write5mTokens: 0, write1hTokens: 0, assumedTtlTokens: 0, sessions: new Map() };
|
|
156
|
+
slice = { write5mTokens: 0, write1hTokens: 0, assumedTtlTokens: 0, overlongUsd: 0, sessions: new Map() };
|
|
142
157
|
slices.set(key, slice);
|
|
143
158
|
}
|
|
144
159
|
|
|
145
160
|
if (record.writeTtlKnown) {
|
|
146
161
|
slice.write5mTokens += record.cacheWrite5mTokens;
|
|
147
162
|
slice.write1hTokens += record.cacheWrite1hTokens;
|
|
163
|
+
if (record.cacheWrite1hTokens > 0) {
|
|
164
|
+
const model = catalogue.byId.get(record.model)!;
|
|
165
|
+
const at = billedAt(record, on);
|
|
166
|
+
const { inputPerMTok } = effectivePricing(model, at, promptSizeOf(record));
|
|
167
|
+
const rates = multipliersFor(model, at);
|
|
168
|
+
slice.overlongUsd +=
|
|
169
|
+
(record.cacheWrite1hTokens / 1_000_000) * inputPerMTok * (rates.cacheWrite1h - rates.cacheWrite5m);
|
|
170
|
+
}
|
|
148
171
|
} else {
|
|
149
172
|
// The flat count sits in the 5m bucket by pricing convention; here it is
|
|
150
173
|
// kept apart, because the whole question is which TTL it really was.
|
|
@@ -192,22 +215,21 @@ export function createTtlFitTracker(options: TtlFitOptions): TtlFitTracker {
|
|
|
192
215
|
* uncertain one and money comes last: writes that expire are broken at
|
|
193
216
|
* any price, and only writes that demonstrably work can be overpaying.
|
|
194
217
|
*/
|
|
218
|
+
/*
|
|
219
|
+
* Each model's own entry lives, not Anthropic's: an OpenAI entry outlives
|
|
220
|
+
* a ten-minute pause that a five-minute one would not.
|
|
221
|
+
*/
|
|
222
|
+
const ttl = cacheTtlOf(model);
|
|
223
|
+
const longer = ttl.longerMs ?? ttl.writeMs;
|
|
195
224
|
let verdict: TtlFitVerdict;
|
|
196
|
-
if (slice.write5mTokens > 0 && gap >
|
|
197
|
-
else if (slice.write1hTokens > 0 && gap >
|
|
198
|
-
else if (slice.assumedTtlTokens > 0 && gap >
|
|
199
|
-
else if (slice.write1hTokens > 0 && gap <=
|
|
200
|
-
else if (slice.assumedTtlTokens > 0 && gap >
|
|
225
|
+
if (slice.write5mTokens > 0 && gap > ttl.writeMs) verdict = 'expires-before-reuse';
|
|
226
|
+
else if (slice.write1hTokens > 0 && gap > longer) verdict = 'expires-before-reuse';
|
|
227
|
+
else if (slice.assumedTtlTokens > 0 && gap > longer) verdict = 'expires-before-reuse';
|
|
228
|
+
else if (slice.write1hTokens > 0 && gap <= ttl.writeMs) verdict = 'overlong-ttl';
|
|
229
|
+
else if (slice.assumedTtlTokens > 0 && gap > ttl.writeMs) verdict = 'unsettled';
|
|
201
230
|
else verdict = 'fits';
|
|
202
231
|
|
|
203
|
-
const
|
|
204
|
-
const rates = multipliersFor(model);
|
|
205
|
-
const overpayUsd =
|
|
206
|
-
verdict === 'overlong-ttl'
|
|
207
|
-
? (slice.write1hTokens / 1_000_000) *
|
|
208
|
-
inputPerMTok *
|
|
209
|
-
(rates.cacheWrite1h - rates.cacheWrite5m)
|
|
210
|
-
: 0;
|
|
232
|
+
const overpayUsd = verdict === 'overlong-ttl' ? slice.overlongUsd : 0;
|
|
211
233
|
|
|
212
234
|
out.push({
|
|
213
235
|
label: key.slice(0, split),
|
|
@@ -220,6 +242,8 @@ export function createTtlFitTracker(options: TtlFitOptions): TtlFitTracker {
|
|
|
220
242
|
write1hTokens: slice.write1hTokens,
|
|
221
243
|
assumedTtlTokens: slice.assumedTtlTokens,
|
|
222
244
|
verdict,
|
|
245
|
+
entryTtlMs: ttl.writeMs,
|
|
246
|
+
longerTtlMs: ttl.longerMs,
|
|
223
247
|
overpayUsd,
|
|
224
248
|
});
|
|
225
249
|
}
|
package/src/types.ts
CHANGED
|
@@ -169,6 +169,12 @@ export type TierCondition =
|
|
|
169
169
|
* `effectivePricing` says which happened rather than guessing.
|
|
170
170
|
*/
|
|
171
171
|
| { kind: 'input-tokens-above'; tokens: number }
|
|
172
|
+
/**
|
|
173
|
+
* The prompt is `tokens` input tokens or more: xAI's wording, "requests
|
|
174
|
+
* whose prompt reaches 200k tokens". The same as above at one token less,
|
|
175
|
+
* kept apart so the sentence a reader sees is the provider's own.
|
|
176
|
+
*/
|
|
177
|
+
| { kind: 'input-tokens-at-least'; tokens: number }
|
|
172
178
|
/**
|
|
173
179
|
* The call falls inside one of `hours` in UTC, optionally on weekdays only.
|
|
174
180
|
*
|
|
@@ -238,6 +244,18 @@ export interface ModelPricing {
|
|
|
238
244
|
caching?: CachingMode;
|
|
239
245
|
/** Provider-specific cache and batch pricing. Defaults to Anthropic's. */
|
|
240
246
|
multipliers?: CostMultipliers;
|
|
247
|
+
/**
|
|
248
|
+
* How long a cache entry lives, for a provider that bills writes.
|
|
249
|
+
*
|
|
250
|
+
* `writeMs` is the life of the entry a `cacheWrite5m` write buys, and
|
|
251
|
+
* `longerMs` the longer entry the provider also sells, or `null` when it
|
|
252
|
+
* sells none. Absent means Anthropic's: five minutes, and an hour at 2x.
|
|
253
|
+
* OpenAI's GPT-5.6 and later write one kind of entry, which lives at least
|
|
254
|
+
* 30 minutes after its last write or read, and the TTL verdict judged it
|
|
255
|
+
* against five minutes until this field existed: a conversation pausing for
|
|
256
|
+
* ten minutes was told its cache expired before it was read.
|
|
257
|
+
*/
|
|
258
|
+
cacheTtl?: { writeMs: number; longerMs: number | null };
|
|
241
259
|
/**
|
|
242
260
|
* Whether to offer this model as a cheaper alternative.
|
|
243
261
|
*
|