@rulvar/core 1.102.0 → 1.103.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/index.d.ts +19 -5
- package/dist/index.js +115 -53
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -828,14 +828,18 @@ interface EntryBillingFold {
|
|
|
828
828
|
/**
|
|
829
829
|
* The billing fold over one terminal entry (RV504), shared by the
|
|
830
830
|
* CostReport and invoice folds so the total, every breakdown, and the
|
|
831
|
-
* per-row prices can never disagree.
|
|
832
|
-
*
|
|
831
|
+
* per-row prices can never disagree. Coverage is decided per MODEL with
|
|
832
|
+
* the symmetric key (RV604): for every model whose per-dispatch
|
|
833
|
+
* `providerCalls` sum to exactly its usage, each call is priced
|
|
833
834
|
* individually, so a nonlinear long-context tier fires per REQUEST,
|
|
834
835
|
* which is the pricing contract's stated semantics; an aggregate that
|
|
835
836
|
* crossed a threshold no single request crossed no longer re-prices
|
|
836
|
-
*
|
|
837
|
-
* no records, or records that do not
|
|
838
|
-
* before: the per-model aggregate
|
|
837
|
+
* that model (the ninth-experiment 52% overreport, and the round-52
|
|
838
|
+
* multi-role default). A model with no records, or records that do not
|
|
839
|
+
* cover its usage, folds exactly as before: the per-model aggregate
|
|
840
|
+
* slices of {@link priceEntryUsage}. `fullyAttributed` is true only
|
|
841
|
+
* when every slice model is covered and no record names a model absent
|
|
842
|
+
* from the slices.
|
|
839
843
|
*/
|
|
840
844
|
declare function priceEntryBilling(entry: JournalEntry, priceUsd: (servedBy: ModelRef, usage: Usage, seq?: number) => number | undefined): EntryBillingFold;
|
|
841
845
|
/**
|
|
@@ -9563,6 +9567,16 @@ interface InvoiceExport {
|
|
|
9563
9567
|
}>;
|
|
9564
9568
|
/** Rows whose reconciliation is not 'provider-id-present'. */
|
|
9565
9569
|
reconciliationFailures: number;
|
|
9570
|
+
/**
|
|
9571
|
+
* USD of allocation pools that had a target and no row to carry it
|
|
9572
|
+
* (RV605). The dust pass refuses to move such dollars onto another
|
|
9573
|
+
* model's rows just to make the column sum, so on the (pathological)
|
|
9574
|
+
* journals where this happens the flat `allocatedUsd` sum reproduces
|
|
9575
|
+
* `totalUsd` minus this amount. Absent when zero, which is every
|
|
9576
|
+
* well-formed journal: the per-slice remainder rows guarantee a row
|
|
9577
|
+
* wherever a slice has usage.
|
|
9578
|
+
*/
|
|
9579
|
+
unallocatedUsd?: number;
|
|
9566
9580
|
/** Rows carrying `usageUnknown`; present when at least one does. */
|
|
9567
9581
|
usageUnknownRows?: number;
|
|
9568
9582
|
/** Present and true when any contributing entry carried approximate usage. */
|
package/dist/index.js
CHANGED
|
@@ -2401,12 +2401,11 @@ const BILLING_FIELDS = [
|
|
|
2401
2401
|
"cacheReadTokens",
|
|
2402
2402
|
"cacheWriteTokens"
|
|
2403
2403
|
];
|
|
2404
|
-
/**
|
|
2405
|
-
function
|
|
2406
|
-
if (slices.length === 0 || records.length === 0) return false;
|
|
2404
|
+
/** Per-model billing-counter sums of either side of the coverage test. */
|
|
2405
|
+
function sumUsageByModel(items) {
|
|
2407
2406
|
const sums = /* @__PURE__ */ new Map();
|
|
2408
|
-
for (const
|
|
2409
|
-
let sum = sums.get(
|
|
2407
|
+
for (const item of items) {
|
|
2408
|
+
let sum = sums.get(item.servedBy);
|
|
2410
2409
|
if (sum === void 0) {
|
|
2411
2410
|
sum = {
|
|
2412
2411
|
fields: {
|
|
@@ -2417,54 +2416,61 @@ function callsCoverSlices(slices, records) {
|
|
|
2417
2416
|
},
|
|
2418
2417
|
reasoning: 0
|
|
2419
2418
|
};
|
|
2420
|
-
sums.set(
|
|
2419
|
+
sums.set(item.servedBy, sum);
|
|
2421
2420
|
}
|
|
2422
|
-
for (const field of BILLING_FIELDS) sum.fields[field] = (sum.fields[field] ?? 0) +
|
|
2423
|
-
sum.reasoning +=
|
|
2421
|
+
for (const field of BILLING_FIELDS) sum.fields[field] = (sum.fields[field] ?? 0) + item.usage[field];
|
|
2422
|
+
sum.reasoning += item.usage.reasoningTokens ?? 0;
|
|
2424
2423
|
}
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2424
|
+
return sums;
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* The models whose records exactly cover their usage, counter for
|
|
2428
|
+
* counter. BOTH sides aggregate by `servedBy` (RV604): the usage slices
|
|
2429
|
+
* split one model's spend by role (a schema fires a same-model extract
|
|
2430
|
+
* by default, so several slices of one model are the ordinary shape),
|
|
2431
|
+
* and comparing a per-role slice against the per-model record sum
|
|
2432
|
+
* refused coverage on exactly that default, re-tiering aggregates no
|
|
2433
|
+
* single request crossed. The symmetric key compares model sum against
|
|
2434
|
+
* model sum, so coverage is per model: a covered model prices per call
|
|
2435
|
+
* while an uncovered one honestly keeps the aggregate basis.
|
|
2436
|
+
*/
|
|
2437
|
+
function coveredModels(slices, records) {
|
|
2438
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2439
|
+
if (slices.length === 0 || records.length === 0) return covered;
|
|
2440
|
+
const recordSums = sumUsageByModel(records);
|
|
2441
|
+
const sliceSums = sumUsageByModel(slices);
|
|
2442
|
+
for (const [model, sliceSum] of sliceSums) {
|
|
2443
|
+
const recordSum = recordSums.get(model);
|
|
2444
|
+
if (recordSum === void 0) continue;
|
|
2445
|
+
if (BILLING_FIELDS.every((field) => recordSum.fields[field] === sliceSum.fields[field]) && recordSum.reasoning === sliceSum.reasoning) covered.add(model);
|
|
2430
2446
|
}
|
|
2431
|
-
|
|
2432
|
-
return true;
|
|
2447
|
+
return covered;
|
|
2433
2448
|
}
|
|
2434
2449
|
/**
|
|
2435
2450
|
* The billing fold over one terminal entry (RV504), shared by the
|
|
2436
2451
|
* CostReport and invoice folds so the total, every breakdown, and the
|
|
2437
|
-
* per-row prices can never disagree.
|
|
2438
|
-
*
|
|
2452
|
+
* per-row prices can never disagree. Coverage is decided per MODEL with
|
|
2453
|
+
* the symmetric key (RV604): for every model whose per-dispatch
|
|
2454
|
+
* `providerCalls` sum to exactly its usage, each call is priced
|
|
2439
2455
|
* individually, so a nonlinear long-context tier fires per REQUEST,
|
|
2440
2456
|
* which is the pricing contract's stated semantics; an aggregate that
|
|
2441
2457
|
* crossed a threshold no single request crossed no longer re-prices
|
|
2442
|
-
*
|
|
2443
|
-
* no records, or records that do not
|
|
2444
|
-
* before: the per-model aggregate
|
|
2458
|
+
* that model (the ninth-experiment 52% overreport, and the round-52
|
|
2459
|
+
* multi-role default). A model with no records, or records that do not
|
|
2460
|
+
* cover its usage, folds exactly as before: the per-model aggregate
|
|
2461
|
+
* slices of {@link priceEntryUsage}. `fullyAttributed` is true only
|
|
2462
|
+
* when every slice model is covered and no record names a model absent
|
|
2463
|
+
* from the slices.
|
|
2445
2464
|
*/
|
|
2446
2465
|
function priceEntryBilling(entry, priceUsd) {
|
|
2447
2466
|
const slices = entryUsageSlices(entry);
|
|
2448
2467
|
const records = entry.providerCalls ?? [];
|
|
2449
|
-
|
|
2450
|
-
const aggregate = priceEntryUsage(entry, priceUsd);
|
|
2451
|
-
return {
|
|
2452
|
-
units: aggregate.priced.map((slice) => ({
|
|
2453
|
-
source: "slice",
|
|
2454
|
-
servedBy: slice.servedBy,
|
|
2455
|
-
usage: slice.usage,
|
|
2456
|
-
...slice.role === void 0 ? {} : { role: slice.role },
|
|
2457
|
-
usd: slice.usd
|
|
2458
|
-
})),
|
|
2459
|
-
usd: aggregate.usd,
|
|
2460
|
-
unpriced: aggregate.unpriced,
|
|
2461
|
-
fullyAttributed: false
|
|
2462
|
-
};
|
|
2463
|
-
}
|
|
2468
|
+
const covered = coveredModels(slices, records);
|
|
2464
2469
|
const units = [];
|
|
2465
2470
|
const unpricedByModel = /* @__PURE__ */ new Map();
|
|
2466
2471
|
let usd = 0;
|
|
2467
2472
|
for (const record of records) {
|
|
2473
|
+
if (!covered.has(record.servedBy)) continue;
|
|
2468
2474
|
const price = priceUsd(record.servedBy, record.usage, entry.seq);
|
|
2469
2475
|
if (price === void 0 || !Number.isFinite(price) || price < 0) {
|
|
2470
2476
|
const sum = unpricedByModel.get(record.servedBy) ?? {
|
|
@@ -2489,6 +2495,32 @@ function priceEntryBilling(entry, priceUsd) {
|
|
|
2489
2495
|
usd: price
|
|
2490
2496
|
});
|
|
2491
2497
|
}
|
|
2498
|
+
for (const slice of slices) {
|
|
2499
|
+
if (covered.has(slice.servedBy)) continue;
|
|
2500
|
+
const price = priceUsd(slice.servedBy, slice.usage, entry.seq);
|
|
2501
|
+
if (price === void 0 || !Number.isFinite(price) || price < 0) {
|
|
2502
|
+
const sum = unpricedByModel.get(slice.servedBy) ?? {
|
|
2503
|
+
inputTokens: 0,
|
|
2504
|
+
outputTokens: 0,
|
|
2505
|
+
cacheReadTokens: 0,
|
|
2506
|
+
cacheWriteTokens: 0
|
|
2507
|
+
};
|
|
2508
|
+
for (const field of BILLING_FIELDS) sum[field] += slice.usage[field];
|
|
2509
|
+
const reasoning = (sum.reasoningTokens ?? 0) + (slice.usage.reasoningTokens ?? 0);
|
|
2510
|
+
if (reasoning > 0) sum.reasoningTokens = reasoning;
|
|
2511
|
+
unpricedByModel.set(slice.servedBy, sum);
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
usd += price;
|
|
2515
|
+
units.push({
|
|
2516
|
+
source: "slice",
|
|
2517
|
+
servedBy: slice.servedBy,
|
|
2518
|
+
usage: slice.usage,
|
|
2519
|
+
...slice.role === void 0 ? {} : { role: slice.role },
|
|
2520
|
+
usd: price
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
const fullyAttributed = slices.length > 0 && slices.every((slice) => covered.has(slice.servedBy)) && records.every((record) => slices.some((slice) => slice.servedBy === record.servedBy));
|
|
2492
2524
|
return {
|
|
2493
2525
|
units,
|
|
2494
2526
|
usd,
|
|
@@ -2496,7 +2528,7 @@ function priceEntryBilling(entry, priceUsd) {
|
|
|
2496
2528
|
servedBy,
|
|
2497
2529
|
usage
|
|
2498
2530
|
})),
|
|
2499
|
-
fullyAttributed
|
|
2531
|
+
fullyAttributed
|
|
2500
2532
|
};
|
|
2501
2533
|
}
|
|
2502
2534
|
/**
|
|
@@ -8404,16 +8436,29 @@ const USAGE_FIELDS = [
|
|
|
8404
8436
|
"cacheReadTokens",
|
|
8405
8437
|
"cacheWriteTokens"
|
|
8406
8438
|
];
|
|
8407
|
-
/**
|
|
8408
|
-
|
|
8439
|
+
/**
|
|
8440
|
+
* One usage slice minus ITS OWN records' sum, clamped at zero per field
|
|
8441
|
+
* (RV605). A record belongs to a slice when it names the same serving
|
|
8442
|
+
* model, and the same role when the slice carries one; a slice written
|
|
8443
|
+
* without a role (the legacy whole-entry fallback) absorbs every record
|
|
8444
|
+
* of its model, which is exactly the pre-RV605 arithmetic for
|
|
8445
|
+
* single-model entries. Computing the remainder per slice instead of
|
|
8446
|
+
* per entry is what keeps an orphaned model's spend on a row of that
|
|
8447
|
+
* model: the whole-entry remainder was published under `entry.servedBy`,
|
|
8448
|
+
* so a slice with no records left its allocation pool rowless and the
|
|
8449
|
+
* dust pass moved its dollars onto another model's row.
|
|
8450
|
+
*/
|
|
8451
|
+
function sliceRemainder(slice, records) {
|
|
8409
8452
|
const remainder = {
|
|
8410
|
-
inputTokens:
|
|
8411
|
-
outputTokens:
|
|
8412
|
-
cacheReadTokens:
|
|
8413
|
-
cacheWriteTokens:
|
|
8453
|
+
inputTokens: slice.usage.inputTokens,
|
|
8454
|
+
outputTokens: slice.usage.outputTokens,
|
|
8455
|
+
cacheReadTokens: slice.usage.cacheReadTokens,
|
|
8456
|
+
cacheWriteTokens: slice.usage.cacheWriteTokens
|
|
8414
8457
|
};
|
|
8415
|
-
let reasoning =
|
|
8458
|
+
let reasoning = slice.usage.reasoningTokens ?? 0;
|
|
8416
8459
|
for (const record of records) {
|
|
8460
|
+
if (record.servedBy !== slice.servedBy) continue;
|
|
8461
|
+
if (slice.role !== void 0 && record.role !== slice.role) continue;
|
|
8417
8462
|
for (const field of USAGE_FIELDS) remainder[field] = Math.max(0, remainder[field] - record.usage[field]);
|
|
8418
8463
|
reasoning = Math.max(0, reasoning - (record.usage.reasoningTokens ?? 0));
|
|
8419
8464
|
}
|
|
@@ -8437,9 +8482,16 @@ function totalTokens(usage) {
|
|
|
8437
8482
|
* association so the flat sum over `rows` reproduces `totalUsd`
|
|
8438
8483
|
* exactly. Rows on unpriced models keep zero: their spend is in
|
|
8439
8484
|
* `unpriced`, not in `totalUsd`.
|
|
8485
|
+
*
|
|
8486
|
+
* A pool with a target and NO rows refuses the transfer (RV605): its
|
|
8487
|
+
* dollars are excluded from the dust reconciliation and returned as the
|
|
8488
|
+
* unallocated share, because dumping them on the globally largest row
|
|
8489
|
+
* moved one model's spend onto another model's line just to make the
|
|
8490
|
+
* column sum. The returned amount is zero on every well-formed journal
|
|
8491
|
+
* (the per-slice remainder rows above guarantee a row wherever a slice
|
|
8492
|
+
* has usage), so the flat sum still reproduces `totalUsd` exactly there.
|
|
8440
8493
|
*/
|
|
8441
8494
|
function allocateRows(rows, entries, priceUsd, totalUsd) {
|
|
8442
|
-
if (rows.length === 0) return;
|
|
8443
8495
|
const targets = /* @__PURE__ */ new Map();
|
|
8444
8496
|
for (const entry of entries) {
|
|
8445
8497
|
if (entry.status === "running" || entry.usage === void 0) continue;
|
|
@@ -8455,6 +8507,9 @@ function allocateRows(rows, entries, priceUsd, totalUsd) {
|
|
|
8455
8507
|
if (pool === void 0) pools.set(key, [row]);
|
|
8456
8508
|
else pool.push(row);
|
|
8457
8509
|
}
|
|
8510
|
+
let unallocated = 0;
|
|
8511
|
+
for (const [key, target] of targets) if (target !== 0 && !pools.has(key)) unallocated += target;
|
|
8512
|
+
if (rows.length === 0) return unallocated;
|
|
8458
8513
|
for (const [key, members] of pools) {
|
|
8459
8514
|
const target = targets.get(key) ?? 0;
|
|
8460
8515
|
if (target === 0) continue;
|
|
@@ -8471,12 +8526,14 @@ function allocateRows(rows, entries, priceUsd, totalUsd) {
|
|
|
8471
8526
|
}
|
|
8472
8527
|
let absorber;
|
|
8473
8528
|
for (const row of rows) if (absorber === void 0 || row.allocatedUsd > absorber.allocatedUsd) absorber = row;
|
|
8474
|
-
if (absorber === void 0) return;
|
|
8529
|
+
if (absorber === void 0) return unallocated;
|
|
8530
|
+
const goal = totalUsd - unallocated;
|
|
8475
8531
|
for (let pass = 0; pass < 8; pass += 1) {
|
|
8476
8532
|
const flat = rows.reduce((acc, row) => acc + row.allocatedUsd, 0);
|
|
8477
|
-
if (flat ===
|
|
8478
|
-
absorber.allocatedUsd +=
|
|
8533
|
+
if (flat === goal) break;
|
|
8534
|
+
absorber.allocatedUsd += goal - flat;
|
|
8479
8535
|
}
|
|
8536
|
+
return unallocated;
|
|
8480
8537
|
}
|
|
8481
8538
|
/** A single row priced at its own model's rate; broken rates fold as unpriced. */
|
|
8482
8539
|
function rowUsd(priceUsd, servedBy, usage, seq) {
|
|
@@ -8548,13 +8605,16 @@ function invoiceFromJournal(entries, priceUsd, options) {
|
|
|
8548
8605
|
});
|
|
8549
8606
|
continue;
|
|
8550
8607
|
}
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
const
|
|
8608
|
+
let remainderOrdinal = records.length + 1;
|
|
8609
|
+
for (const slice of entryUsageSlices(entry)) {
|
|
8610
|
+
const remainder = sliceRemainder(slice, records);
|
|
8611
|
+
if (remainder === void 0) continue;
|
|
8612
|
+
const usd = rowUsd(priceUsd, slice.servedBy, remainder, entry.seq);
|
|
8554
8613
|
rows.push({
|
|
8555
8614
|
...base,
|
|
8556
|
-
ordinal:
|
|
8557
|
-
servedBy:
|
|
8615
|
+
ordinal: remainderOrdinal,
|
|
8616
|
+
servedBy: slice.servedBy,
|
|
8617
|
+
...slice.role === void 0 ? {} : { role: slice.role },
|
|
8558
8618
|
outcome: "unattributed",
|
|
8559
8619
|
usage: remainder,
|
|
8560
8620
|
...entry.usageApprox === true ? { usageApprox: true } : {},
|
|
@@ -8563,9 +8623,10 @@ function invoiceFromJournal(entries, priceUsd, options) {
|
|
|
8563
8623
|
...mark,
|
|
8564
8624
|
reconciliation: "unattributed"
|
|
8565
8625
|
});
|
|
8626
|
+
remainderOrdinal += 1;
|
|
8566
8627
|
}
|
|
8567
8628
|
}
|
|
8568
|
-
allocateRows(rows, entries, priceUsd, report.grossUsd);
|
|
8629
|
+
const unallocatedUsd = allocateRows(rows, entries, priceUsd, report.grossUsd);
|
|
8569
8630
|
const usageApprox = report.usageApprox === true || report.abandoned.usageApprox === true;
|
|
8570
8631
|
return {
|
|
8571
8632
|
rows,
|
|
@@ -8574,6 +8635,7 @@ function invoiceFromJournal(entries, priceUsd, options) {
|
|
|
8574
8635
|
abandonedUsd: report.abandoned.usd,
|
|
8575
8636
|
pricingBasis: "per-call",
|
|
8576
8637
|
rowUsdNonAdditive: !everyEntryFullyAttributed,
|
|
8638
|
+
...unallocatedUsd === 0 ? {} : { unallocatedUsd },
|
|
8577
8639
|
unpriced: [...report.unpriced, ...report.abandoned.unpriced],
|
|
8578
8640
|
reconciliationFailures: rows.filter((row) => row.reconciliation !== "provider-id-present").length,
|
|
8579
8641
|
...(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.103.0",
|
|
4
4
|
"description": "Rulvar core: L0 contracts, journal kernel, ctx primitives, agent runtime, model router, tool system, dynamic orchestrator, InMemory and JSONL stores, event stream.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|