@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/connector.ts
CHANGED
|
@@ -379,20 +379,23 @@ export function normalizeOpenAIUsage(payload: unknown): ConnectorPull {
|
|
|
379
379
|
}
|
|
380
380
|
const cached = num(r.input_cached_tokens) ?? 0;
|
|
381
381
|
const input = num(r.input_tokens) ?? 0;
|
|
382
|
+
// The 30-minute write GPT-5.6 and later bill at 1.25x. The 12-hour write
|
|
383
|
+
// has no published price and stays in the input total, a floor.
|
|
384
|
+
const written = num(r.input_cache_write_tokens) ?? 0;
|
|
382
385
|
buckets.push({
|
|
383
386
|
fromMs,
|
|
384
387
|
toMs,
|
|
385
388
|
model,
|
|
386
389
|
calls: num(r.num_model_requests),
|
|
387
|
-
// OpenAI reports cached tokens *inside* the input total, so
|
|
388
|
-
// uncached
|
|
389
|
-
// would bill
|
|
390
|
-
inputTokens: Math.max(0, input - cached),
|
|
390
|
+
// OpenAI reports cached tokens and writes *inside* the input total, so
|
|
391
|
+
// the uncached part is the subtraction. Reporting them at face value
|
|
392
|
+
// would bill them twice.
|
|
393
|
+
inputTokens: Math.max(0, input - cached - written),
|
|
391
394
|
cacheReadTokens: cached,
|
|
392
|
-
cacheWrite5mTokens:
|
|
395
|
+
cacheWrite5mTokens: written,
|
|
393
396
|
cacheWrite1hTokens: 0,
|
|
394
|
-
// Nothing was assumed:
|
|
395
|
-
// absent field is not an assumed TTL.
|
|
397
|
+
// Nothing was assumed: an OpenAI write has one TTL, which the model
|
|
398
|
+
// states (`cacheTtl`), and an absent field is not an assumed TTL.
|
|
396
399
|
writeTtlKnown: true,
|
|
397
400
|
outputTokens: num(r.output_tokens) ?? 0,
|
|
398
401
|
group: groupOf(r, ['project_id', 'api_key_id', 'batch']),
|
|
@@ -444,6 +447,18 @@ export interface BucketedSlice {
|
|
|
444
447
|
/** Writes priced at the 1-hour rate, when the source did not state the TTL. */
|
|
445
448
|
cacheWriteUsdIfAssumed1h: number;
|
|
446
449
|
writeTtlKnown: boolean;
|
|
450
|
+
/**
|
|
451
|
+
* Buckets billed at the dearer of two rates, and what they cost each way.
|
|
452
|
+
*
|
|
453
|
+
* A model priced by prompt length bills each request on its own prompt; a
|
|
454
|
+
* bucket sums requests and says none of their lengths. Unless the bucket's
|
|
455
|
+
* whole prompt total is at or under the line, it is billed at the dearer
|
|
456
|
+
* rate, so `totalUsd` is a ceiling on it: `tierCeilingUsd` is its share of
|
|
457
|
+
* that total and `tierFloorUsd` the same tokens at the cheaper rate.
|
|
458
|
+
*/
|
|
459
|
+
tierCeilingBuckets: number;
|
|
460
|
+
tierCeilingUsd: number;
|
|
461
|
+
tierFloorUsd: number;
|
|
447
462
|
}
|
|
448
463
|
|
|
449
464
|
export interface BucketedReport {
|
|
@@ -480,9 +495,15 @@ export interface BucketedReport {
|
|
|
480
495
|
*/
|
|
481
496
|
export function bucketedProfile(
|
|
482
497
|
pull: ConnectorPull,
|
|
498
|
+
/**
|
|
499
|
+
* `on` is accepted and no longer read. Each bucket is billed at its own
|
|
500
|
+
* start, as `profile` bills each call at its own moment: priced at the
|
|
501
|
+
* report's date, a pull spanning a published price change came out entirely
|
|
502
|
+
* at whichever price applied the day it ran.
|
|
503
|
+
*/
|
|
483
504
|
options: { catalogue: PricingCatalogue; on?: Date },
|
|
484
505
|
): BucketedReport {
|
|
485
|
-
const { catalogue
|
|
506
|
+
const { catalogue } = options;
|
|
486
507
|
|
|
487
508
|
const slices = new Map<string, BucketedSlice>();
|
|
488
509
|
const days = new Map<string, { usd: number; calls: number | null }>();
|
|
@@ -498,9 +519,28 @@ export function bucketedProfile(
|
|
|
498
519
|
continue;
|
|
499
520
|
}
|
|
500
521
|
|
|
501
|
-
const
|
|
502
|
-
|
|
522
|
+
const at = new Date(bucket.fromMs);
|
|
523
|
+
/*
|
|
524
|
+
* A bucket of exactly one request says that request's prompt; any other
|
|
525
|
+
* bucket only bounds it. See `PromptSize`.
|
|
526
|
+
*/
|
|
527
|
+
const prompt =
|
|
528
|
+
bucket.inputTokens + bucket.cacheReadTokens + bucket.cacheWrite5mTokens + bucket.cacheWrite1hTokens;
|
|
529
|
+
const priced = effectivePricing(
|
|
530
|
+
model,
|
|
531
|
+
at,
|
|
532
|
+
bucket.calls === 1 ? { inputTokens: prompt } : { inputTokensAtMost: prompt },
|
|
533
|
+
);
|
|
534
|
+
const { inputPerMTok, outputPerMTok } = priced;
|
|
535
|
+
const rates = multipliersFor(model, at);
|
|
503
536
|
const per = (count: number, rate: number): number => (count / 1_000_000) * rate;
|
|
537
|
+
const costAt = (input: number, output: number): number =>
|
|
538
|
+
per(bucket.inputTokens, input) +
|
|
539
|
+
per(bucket.cacheReadTokens, input * rates.cacheRead) +
|
|
540
|
+
per(bucket.cacheWrite5mTokens, input * rates.cacheWrite5m) +
|
|
541
|
+
per(bucket.cacheWrite1hTokens, input * rates.cacheWrite1h) +
|
|
542
|
+
per(bucket.outputTokens, output);
|
|
543
|
+
const floor = priced.tier?.decided === false ? priced.tier.floor : undefined;
|
|
504
544
|
|
|
505
545
|
const inputUsd = per(bucket.inputTokens, inputPerMTok);
|
|
506
546
|
const cacheReadUsd = per(bucket.cacheReadTokens, inputPerMTok * rates.cacheRead);
|
|
@@ -529,6 +569,9 @@ export function bucketedProfile(
|
|
|
529
569
|
cachedTokensAtInputRateUsd: 0,
|
|
530
570
|
cacheWriteUsdIfAssumed1h: 0,
|
|
531
571
|
writeTtlKnown: true,
|
|
572
|
+
tierCeilingBuckets: 0,
|
|
573
|
+
tierCeilingUsd: 0,
|
|
574
|
+
tierFloorUsd: 0,
|
|
532
575
|
};
|
|
533
576
|
slice.calls = slice.calls === null || bucket.calls === null ? null : slice.calls + bucket.calls;
|
|
534
577
|
slice.inputTokens += bucket.inputTokens;
|
|
@@ -543,6 +586,11 @@ export function bucketedProfile(
|
|
|
543
586
|
slice.cachedTokensAtInputRateUsd += atInputRate;
|
|
544
587
|
slice.cacheWriteUsdIfAssumed1h += ifAssumed1h;
|
|
545
588
|
slice.writeTtlKnown = slice.writeTtlKnown && bucket.writeTtlKnown;
|
|
589
|
+
if (floor !== undefined) {
|
|
590
|
+
slice.tierCeilingBuckets += 1;
|
|
591
|
+
slice.tierCeilingUsd += costAt(inputPerMTok, outputPerMTok);
|
|
592
|
+
slice.tierFloorUsd += costAt(floor.inputPerMTok, floor.outputPerMTok);
|
|
593
|
+
}
|
|
546
594
|
slices.set(bucket.model, slice);
|
|
547
595
|
|
|
548
596
|
const day = new Date(bucket.fromMs).toISOString().slice(0, 10);
|
package/src/conversation.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';
|
|
@@ -95,8 +96,9 @@ export interface ConversationOptions {
|
|
|
95
96
|
function inputCostOf(record: UsageRecord, catalogue: PricingCatalogue, on: Date): number | null {
|
|
96
97
|
const model = catalogue.byId.get(record.model);
|
|
97
98
|
if (!model) return null;
|
|
98
|
-
const
|
|
99
|
-
const
|
|
99
|
+
const at = billedAt(record, on);
|
|
100
|
+
const { inputPerMTok } = effectivePricing(model, at, promptSizeOf(record));
|
|
101
|
+
const rates = multipliersFor(model, at);
|
|
100
102
|
const per = (tokens: number, rate: number): number => (tokens / 1_000_000) * rate;
|
|
101
103
|
return (
|
|
102
104
|
per(record.inputTokens, inputPerMTok) +
|
package/src/gateway.ts
CHANGED
|
@@ -216,7 +216,8 @@ function priceCall(
|
|
|
216
216
|
on: Date,
|
|
217
217
|
): number | null {
|
|
218
218
|
if (model === undefined || call.inputTokens === null) return null;
|
|
219
|
-
|
|
219
|
+
// The call's own prompt decides a model priced by prompt length.
|
|
220
|
+
const rates = effectivePricing(model, on, { inputTokens: call.inputTokens });
|
|
220
221
|
const output = call.maxOutputTokens ?? 0;
|
|
221
222
|
return (call.inputTokens / 1_000_000) * rates.inputPerMTok + (output / 1_000_000) * rates.outputPerMTok;
|
|
222
223
|
}
|
|
@@ -236,11 +237,12 @@ function alternativesFor(
|
|
|
236
237
|
const mine = priceCall(model, call, on);
|
|
237
238
|
if (mine === null) return [];
|
|
238
239
|
const out: GatewayAlternative[] = [];
|
|
239
|
-
const
|
|
240
|
+
const size = call.inputTokens === null ? {} : { inputTokens: call.inputTokens };
|
|
241
|
+
const here = effectivePricing(model, on, size);
|
|
240
242
|
|
|
241
243
|
for (const candidate of catalogue.byId.values()) {
|
|
242
244
|
if (candidate.id === model.id || candidate.provider !== model.provider) continue;
|
|
243
|
-
const there = effectivePricing(candidate, on);
|
|
245
|
+
const there = effectivePricing(candidate, on, size);
|
|
244
246
|
if (there.inputPerMTok >= here.inputPerMTok) continue;
|
|
245
247
|
if (call.inputTokens !== null && candidate.contextWindow < call.inputTokens) continue;
|
|
246
248
|
const routed = priceCall(candidate, call, on);
|
package/src/guard.ts
CHANGED
|
@@ -91,18 +91,20 @@ function cheaperThan(
|
|
|
91
91
|
inputTokens: number,
|
|
92
92
|
on: Date,
|
|
93
93
|
): ModelPricing[] {
|
|
94
|
-
|
|
94
|
+
// At this call's prompt, so a model priced by prompt length is ranked at its real band.
|
|
95
|
+
const size = { inputTokens };
|
|
96
|
+
const here = effectivePricing(model, on, size);
|
|
95
97
|
return [...catalogue.byId.values()]
|
|
96
98
|
.filter((candidate) => {
|
|
97
99
|
if (candidate.id === model.id) return false;
|
|
98
100
|
if (candidate.provider !== model.provider) return false;
|
|
99
|
-
const there = effectivePricing(candidate, on);
|
|
101
|
+
const there = effectivePricing(candidate, on, size);
|
|
100
102
|
if (there.inputPerMTok >= here.inputPerMTok) return false;
|
|
101
103
|
// A model the prompt does not fit in is not a cheaper way to make this
|
|
102
104
|
// call; it is a way not to make it.
|
|
103
105
|
return candidate.contextWindow >= inputTokens;
|
|
104
106
|
})
|
|
105
|
-
.sort((a, b) => effectivePricing(b, on).inputPerMTok - effectivePricing(a, on).inputPerMTok);
|
|
107
|
+
.sort((a, b) => effectivePricing(b, on, size).inputPerMTok - effectivePricing(a, on, size).inputPerMTok);
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
export interface GuardRequest extends AnswerRequest {
|
|
@@ -133,9 +135,9 @@ export function guardSpend(
|
|
|
133
135
|
if (model !== undefined && cost.call !== null) {
|
|
134
136
|
const inputTokens = cost.call.inputTokens;
|
|
135
137
|
const outputTokens = cost.call.outputTokens;
|
|
136
|
-
const here = effectivePricing(model, on);
|
|
138
|
+
const here = effectivePricing(model, on, { inputTokens });
|
|
137
139
|
const priceOf = (candidate: ModelPricing): number => {
|
|
138
|
-
const rates = effectivePricing(candidate, on);
|
|
140
|
+
const rates = effectivePricing(candidate, on, { inputTokens });
|
|
139
141
|
return (inputTokens / 1_000_000) * rates.inputPerMTok + (outputTokens / 1_000_000) * rates.outputPerMTok;
|
|
140
142
|
};
|
|
141
143
|
const mine = (inputTokens / 1_000_000) * here.inputPerMTok + (outputTokens / 1_000_000) * here.outputPerMTok;
|
package/src/index.ts
CHANGED
|
@@ -435,7 +435,7 @@ export {
|
|
|
435
435
|
multipliersFor,
|
|
436
436
|
isOffered,
|
|
437
437
|
} from './pricing.js';
|
|
438
|
-
export type { PricingCatalogue } from './pricing.js';
|
|
438
|
+
export type { PricingCatalogue, PromptSize } from './pricing.js';
|
|
439
439
|
export type { CachingMode, Capability, CostMultipliers } from './types.js';
|
|
440
440
|
|
|
441
441
|
// Local price corrections, so a price change needs no library upgrade.
|
package/src/input-shape.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';
|
|
@@ -190,8 +191,9 @@ export function createInputShapeTracker(options: InputShapeOptions): InputShapeT
|
|
|
190
191
|
slices.set(key, slice);
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
const
|
|
194
|
-
const
|
|
194
|
+
const at = billedAt(record, on);
|
|
195
|
+
const { inputPerMTok } = effectivePricing(model, at, promptSizeOf(record));
|
|
196
|
+
const rates = multipliersFor(model, at);
|
|
195
197
|
const per = (count: number, rate: number): number => (count / 1_000_000) * rate;
|
|
196
198
|
|
|
197
199
|
slice.calls += 1;
|
package/src/ladder.ts
CHANGED
|
@@ -151,7 +151,8 @@ export function ladderArithmetic(
|
|
|
151
151
|
if (id === undefined) return null;
|
|
152
152
|
const model = catalogue.byId.get(id);
|
|
153
153
|
if (model === undefined) return null;
|
|
154
|
-
|
|
154
|
+
// The shape's own prompt decides a model priced by prompt length.
|
|
155
|
+
const rates = effectivePricing(model, on, { inputTokens: shape.inputTokens });
|
|
155
156
|
return (
|
|
156
157
|
(shape.inputTokens / 1_000_000) * rates.inputPerMTok +
|
|
157
158
|
(shape.outputTokens / 1_000_000) * rates.outputPerMTok
|
package/src/levers.ts
CHANGED
|
@@ -150,8 +150,11 @@ function repriceAt(
|
|
|
150
150
|
model: ModelPricing,
|
|
151
151
|
on: Date,
|
|
152
152
|
): { inputUsd: number; outputUsd: number; cacheUsd: number; totalUsd: number } {
|
|
153
|
-
|
|
154
|
-
const
|
|
153
|
+
// The breakdown's largest call bounds every prompt in it: see `PromptSize`.
|
|
154
|
+
const { inputPerMTok, outputPerMTok } = effectivePricing(model, on, {
|
|
155
|
+
inputTokensAtMost: breakdown.maxCallInputTokens,
|
|
156
|
+
});
|
|
157
|
+
const rates = multipliersFor(model, on);
|
|
155
158
|
const per = (tokens: number, rate: number): number => (tokens / 1_000_000) * rate;
|
|
156
159
|
|
|
157
160
|
const inputUsd = per(breakdown.inputTokens, inputPerMTok);
|
package/src/openai-usage.ts
CHANGED
|
@@ -95,10 +95,21 @@ export interface OpenaiUsageRecord {
|
|
|
95
95
|
model: string;
|
|
96
96
|
ts: string;
|
|
97
97
|
label?: string;
|
|
98
|
+
/**
|
|
99
|
+
* `num_model_requests` when the bucket sums more than one, `null` when the
|
|
100
|
+
* row does not carry a usable count, and absent when it is exactly one
|
|
101
|
+
* request, which is then a call like any other. See `UsageRecord.requests`.
|
|
102
|
+
*/
|
|
103
|
+
requests?: number | null;
|
|
98
104
|
usage: {
|
|
99
105
|
prompt_tokens: number;
|
|
100
106
|
completion_tokens: number;
|
|
101
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Both inside `prompt_tokens`, as OpenAI counts them, and both taken out of
|
|
109
|
+
* it by `parseUsageLine`. `cache_write_tokens` is the 30-minute write
|
|
110
|
+
* GPT-5.6 and later bill at 1.25x input.
|
|
111
|
+
*/
|
|
112
|
+
prompt_tokens_details?: { cached_tokens: number; cache_write_tokens?: number };
|
|
102
113
|
};
|
|
103
114
|
}
|
|
104
115
|
|
|
@@ -128,6 +139,12 @@ export interface OpenaiUsageConversion {
|
|
|
128
139
|
nonTextTokens: number;
|
|
129
140
|
/** Cache-write tokens on mixed rows, left out because the schema gives them no modality. */
|
|
130
141
|
cacheWriteUnplaced: number;
|
|
142
|
+
/**
|
|
143
|
+
* Tokens written to the 12-hour cache. No price for that write is published
|
|
144
|
+
* anywhere this table can read, so they stay in the prompt total and are
|
|
145
|
+
* priced as plain input, a floor, and named rather than guessed at.
|
|
146
|
+
*/
|
|
147
|
+
cacheWrite12hTokens: number;
|
|
131
148
|
/** Mixed rows the report gave no text split for, refused whole. */
|
|
132
149
|
unsplitRows: number;
|
|
133
150
|
/** `has_more`: this is one page and the bill from it is understated. */
|
|
@@ -182,6 +199,7 @@ export function openaiUsageRecords(
|
|
|
182
199
|
mixedRows: 0,
|
|
183
200
|
nonTextTokens: 0,
|
|
184
201
|
cacheWriteUnplaced: 0,
|
|
202
|
+
cacheWrite12hTokens: 0,
|
|
185
203
|
unsplitRows: 0,
|
|
186
204
|
truncated: false,
|
|
187
205
|
labelledByProject: 0,
|
|
@@ -213,6 +231,7 @@ export function openaiUsageRecords(
|
|
|
213
231
|
let mixedRows = 0;
|
|
214
232
|
let nonTextTokens = 0;
|
|
215
233
|
let cacheWriteUnplaced = 0;
|
|
234
|
+
let cacheWrite12hTokens = 0;
|
|
216
235
|
let unsplitRows = 0;
|
|
217
236
|
let labelledByProject = 0;
|
|
218
237
|
let unruledProject = 0;
|
|
@@ -279,10 +298,18 @@ export function openaiUsageRecords(
|
|
|
279
298
|
const nonText = NON_TEXT.reduce((sum, field) => sum + count(result[field]), 0);
|
|
280
299
|
let prompt: number;
|
|
281
300
|
let cached: number;
|
|
301
|
+
let written = 0;
|
|
282
302
|
let completion: number;
|
|
283
303
|
if (nonText === 0) {
|
|
284
304
|
prompt = count(result.input_tokens);
|
|
285
305
|
cached = count(result.input_cached_tokens);
|
|
306
|
+
/*
|
|
307
|
+
`input_tokens` is uncached + cached + both writes, by the schema's own
|
|
308
|
+
example. The 30-minute write has a price and goes where the parser
|
|
309
|
+
prices it; the 12-hour write has none here and stays in the total.
|
|
310
|
+
*/
|
|
311
|
+
written = count(result.input_cache_write_tokens);
|
|
312
|
+
cacheWrite12hTokens += count(result.input_cache_write_12h_tokens);
|
|
286
313
|
completion = count(result.output_tokens);
|
|
287
314
|
} else {
|
|
288
315
|
const split =
|
|
@@ -306,10 +333,18 @@ export function openaiUsageRecords(
|
|
|
306
333
|
model: result.model,
|
|
307
334
|
ts,
|
|
308
335
|
...(label === undefined ? {} : { label }),
|
|
336
|
+
...requestsField(result.num_model_requests),
|
|
309
337
|
usage: {
|
|
310
338
|
prompt_tokens: prompt,
|
|
311
339
|
completion_tokens: completion,
|
|
312
|
-
...(cached > 0
|
|
340
|
+
...(cached > 0 || written > 0
|
|
341
|
+
? {
|
|
342
|
+
prompt_tokens_details: {
|
|
343
|
+
cached_tokens: cached,
|
|
344
|
+
...(written > 0 ? { cache_write_tokens: written } : {}),
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
: {}),
|
|
313
348
|
},
|
|
314
349
|
});
|
|
315
350
|
rows += 1;
|
|
@@ -330,6 +365,7 @@ export function openaiUsageRecords(
|
|
|
330
365
|
mixedRows,
|
|
331
366
|
nonTextTokens,
|
|
332
367
|
cacheWriteUnplaced,
|
|
368
|
+
cacheWrite12hTokens,
|
|
333
369
|
unsplitRows,
|
|
334
370
|
truncated: report.has_more === true,
|
|
335
371
|
labelledByProject,
|
|
@@ -351,3 +387,9 @@ export function looksLikeOpenaiUsage(text: string, prefixBytes = 8192): boolean
|
|
|
351
387
|
if (head.includes('"organization.usage.completions.result"')) return true;
|
|
352
388
|
return head.includes('"num_model_requests"') && head.includes('"start_time"');
|
|
353
389
|
}
|
|
390
|
+
|
|
391
|
+
/** A bucket's request count as `UsageRecord.requests` reads it: one is a call. */
|
|
392
|
+
function requestsField(value: unknown): { requests?: number | null } {
|
|
393
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) return { requests: null };
|
|
394
|
+
return value === 1 ? {} : { requests: value };
|
|
395
|
+
}
|
|
@@ -63,6 +63,12 @@ export interface OpenrouterActivityRecord {
|
|
|
63
63
|
model: string;
|
|
64
64
|
ts: string;
|
|
65
65
|
label?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The day's `requests` when it is more than one, `null` when the row does
|
|
68
|
+
* not carry a usable count, absent when it is exactly one request. See
|
|
69
|
+
* `UsageRecord.requests`.
|
|
70
|
+
*/
|
|
71
|
+
requests?: number | null;
|
|
66
72
|
usage: { prompt_tokens: number; completion_tokens: number };
|
|
67
73
|
}
|
|
68
74
|
|
|
@@ -186,6 +192,7 @@ export function openrouterActivityRecords(
|
|
|
186
192
|
model: row.model,
|
|
187
193
|
ts: `${row.date}T00:00:00.000Z`,
|
|
188
194
|
...(label === undefined ? {} : { label }),
|
|
195
|
+
...requestsField(row.requests),
|
|
189
196
|
usage: {
|
|
190
197
|
prompt_tokens: count(row.prompt_tokens),
|
|
191
198
|
completion_tokens: count(row.completion_tokens),
|
|
@@ -219,3 +226,9 @@ export function looksLikeOpenrouterActivity(text: string, prefixBytes = 8192): b
|
|
|
219
226
|
const head = text.slice(0, prefixBytes);
|
|
220
227
|
return head.includes('"model_permaslug"') || head.includes('"byok_usage_inference"');
|
|
221
228
|
}
|
|
229
|
+
|
|
230
|
+
/** A day's request count as `UsageRecord.requests` reads it: one is a call. */
|
|
231
|
+
function requestsField(value: unknown): { requests?: number | null } {
|
|
232
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) return { requests: null };
|
|
233
|
+
return value === 1 ? {} : { requests: value };
|
|
234
|
+
}
|
package/src/output-shape.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { effectivePricing } 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';
|
|
@@ -172,7 +173,7 @@ export function createOutputShapeTracker(options: OutputShapeOptions): OutputSha
|
|
|
172
173
|
slices.set(key, slice);
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
const { outputPerMTok } = effectivePricing(model, on);
|
|
176
|
+
const { outputPerMTok } = effectivePricing(model, billedAt(record, on), promptSizeOf(record));
|
|
176
177
|
slice.calls += 1;
|
|
177
178
|
slice.outputTokens += record.outputTokens;
|
|
178
179
|
slice.outputUsd += (record.outputTokens / 1_000_000) * outputPerMTok;
|