@rulvar/core 1.66.0 → 1.67.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 CHANGED
@@ -8416,8 +8416,17 @@ declare class FileTranscriptStore implements TranscriptStore {
8416
8416
  }
8417
8417
  //#endregion
8418
8418
  //#region src/engine/invoice.d.ts
8419
- /** How a row lines up against a provider invoice. */
8420
- type InvoiceReconciliation = "matched" | "missing-provider-id" | "unconfirmed" | "unattributed";
8419
+ /**
8420
+ * How far a row's identity goes toward provider-side reconciliation.
8421
+ * `provider-id-present` asserts exactly what it names: the adapter
8422
+ * surfaced the provider's response id for this call, the join key a
8423
+ * host needs to line the row up against a provider statement. It does
8424
+ * NOT assert any statement, amount, or usage match: the library never
8425
+ * sees provider billing data, so those deeper reconciliation tiers are
8426
+ * host-side joins keyed on `responseId`, not verdicts this export can
8427
+ * make.
8428
+ */
8429
+ type InvoiceReconciliation = "provider-id-present" | "missing-provider-id" | "unconfirmed" | "unattributed";
8421
8430
  /** One billable provider call (or an unattributed usage remainder). */
8422
8431
  interface InvoiceRow {
8423
8432
  /** The terminal journal entry the row folds from. */
@@ -8436,6 +8445,15 @@ interface InvoiceRow {
8436
8445
  usageApprox?: boolean;
8437
8446
  /** This row priced at its own model's rate; absent when no price row covers it. */
8438
8447
  usd?: number;
8448
+ /**
8449
+ * The additive FinOps column: this row's share of `totalUsd`, always
8450
+ * present (zero for rows on unpriced models). Shares are computed
8451
+ * within the row's own (entry, serving model) slice of the same
8452
+ * gross fold the totals run, proportional to per-row `usd`, and one
8453
+ * row absorbs the IEEE rounding dust, so summing `allocatedUsd` over
8454
+ * `rows` reproduces `totalUsd` exactly where summing `usd` does not.
8455
+ */
8456
+ allocatedUsd: number;
8439
8457
  /** The row lies under an abandoned subtree: in grossUsd, not in netUsd. */
8440
8458
  abandoned?: true;
8441
8459
  reconciliation: InvoiceReconciliation;
@@ -8449,12 +8467,25 @@ interface InvoiceExport {
8449
8467
  netUsd: number;
8450
8468
  /** The abandoned share: totalUsd - netUsd, equals CostReport.abandoned.usd. */
8451
8469
  abandonedUsd: number;
8470
+ /**
8471
+ * How per-row `usd` was computed: each call priced individually at
8472
+ * the current table's rates. Always `'per-call'` today; declared so
8473
+ * finance tooling never has to guess the basis.
8474
+ */
8475
+ pricingBasis: "per-call";
8476
+ /**
8477
+ * Always true: per-call `usd` values need not sum to `totalUsd`,
8478
+ * because a nonlinear price table prices an aggregate differently
8479
+ * from the sum of its parts. Sum `allocatedUsd` instead; it exists
8480
+ * precisely so a column sums to the total.
8481
+ */
8482
+ rowUsdNonAdditive: true;
8452
8483
  /** Usage on models absent from pricing, net and abandoned alike; never a silent zero. */
8453
8484
  unpriced: Array<{
8454
8485
  model: string;
8455
8486
  usage: Usage;
8456
8487
  }>;
8457
- /** Rows whose reconciliation is not 'matched'. */
8488
+ /** Rows whose reconciliation is not 'provider-id-present'. */
8458
8489
  reconciliationFailures: number;
8459
8490
  /** Present and true when any contributing entry carried approximate usage. */
8460
8491
  usageApprox?: boolean;
package/dist/index.js CHANGED
@@ -8233,9 +8233,12 @@ function costReportFromJournal(entries, priceUsd) {
8233
8233
  * against the provider's invoice. The totals are the SAME slice fold
8234
8234
  * `costReportFromJournal` runs, so `totalUsd` here equals
8235
8235
  * `CostReport.grossUsd` (and `netUsd` equals `CostReport.totalUsd`)
8236
- * exactly, never approximately; per-row `usd` prices each call
8237
- * individually and is informational, since a nonlinear price table
8238
- * (long-context tiers) prices a split differently from its sum.
8236
+ * exactly, never approximately. The export is self-describing about
8237
+ * its pricing: `pricingBasis` says per-row `usd` prices each call
8238
+ * individually, `rowUsdNonAdditive` says those values need not sum to
8239
+ * `totalUsd` (a nonlinear price table, long-context tiers, prices a
8240
+ * split differently from its sum), and per-row `allocatedUsd` is the
8241
+ * additive column whose flat sum reproduces `totalUsd` exactly.
8239
8242
  *
8240
8243
  * Coverage is loss-free by construction: an entry whose records do not
8241
8244
  * cover its usage total (a resume restored from a checkpoint written
@@ -8274,6 +8277,64 @@ function usageRemainder(total, records) {
8274
8277
  if (reasoning > 0) remainder.reasoningTokens = reasoning;
8275
8278
  return USAGE_FIELDS.some((field) => remainder[field] > 0) || (remainder.reasoningTokens ?? 0) > 0 ? remainder : void 0;
8276
8279
  }
8280
+ /** One allocation pool per (entry, serving model) slice of the gross fold. */
8281
+ function allocationKey(entrySeq, servedBy) {
8282
+ return `${String(entrySeq)} ${servedBy}`;
8283
+ }
8284
+ /** The token-count fallback weight when every row of a pool priced to zero. */
8285
+ function totalTokens(usage) {
8286
+ return usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens + (usage.reasoningTokens ?? 0);
8287
+ }
8288
+ /**
8289
+ * The additive allocation pass: distributes each (entry, model) slice
8290
+ * total of the SAME gross fold the invoice totals run across that
8291
+ * slice's rows, proportional to per-row `usd` (token counts when every
8292
+ * row priced to zero, equal shares when even those are zero), then
8293
+ * lets the largest row absorb the IEEE rounding dust of the fold's own
8294
+ * association so the flat sum over `rows` reproduces `totalUsd`
8295
+ * exactly. Rows on unpriced models keep zero: their spend is in
8296
+ * `unpriced`, not in `totalUsd`.
8297
+ */
8298
+ function allocateRows(rows, entries, priceUsd, totalUsd) {
8299
+ if (rows.length === 0) return;
8300
+ const targets = /* @__PURE__ */ new Map();
8301
+ for (const entry of entries) {
8302
+ if (entry.status === "running" || entry.usage === void 0) continue;
8303
+ for (const slice of priceEntryUsage(entry, priceUsd).priced) {
8304
+ const key = allocationKey(entry.seq, slice.servedBy);
8305
+ targets.set(key, (targets.get(key) ?? 0) + slice.usd);
8306
+ }
8307
+ }
8308
+ const pools = /* @__PURE__ */ new Map();
8309
+ for (const row of rows) {
8310
+ const key = allocationKey(row.entrySeq, row.servedBy);
8311
+ const pool = pools.get(key);
8312
+ if (pool === void 0) pools.set(key, [row]);
8313
+ else pool.push(row);
8314
+ }
8315
+ for (const [key, members] of pools) {
8316
+ const target = targets.get(key) ?? 0;
8317
+ if (target === 0) continue;
8318
+ let weights = members.map((row) => row.usd ?? 0);
8319
+ let sum = weights.reduce((acc, weight) => acc + weight, 0);
8320
+ if (sum === 0) {
8321
+ weights = members.map((row) => totalTokens(row.usage));
8322
+ sum = weights.reduce((acc, weight) => acc + weight, 0);
8323
+ }
8324
+ members.forEach((row, index) => {
8325
+ const weight = weights[index] ?? 0;
8326
+ row.allocatedUsd = sum === 0 ? target / members.length : target * (weight / sum);
8327
+ });
8328
+ }
8329
+ let absorber;
8330
+ for (const row of rows) if (absorber === void 0 || row.allocatedUsd > absorber.allocatedUsd) absorber = row;
8331
+ if (absorber === void 0) return;
8332
+ for (let pass = 0; pass < 8; pass += 1) {
8333
+ const flat = rows.reduce((acc, row) => acc + row.allocatedUsd, 0);
8334
+ if (flat === totalUsd) break;
8335
+ absorber.allocatedUsd += totalUsd - flat;
8336
+ }
8337
+ }
8277
8338
  /** A single row priced at its own model's rate; broken rates fold as unpriced. */
8278
8339
  function rowUsd(priceUsd, servedBy, usage) {
8279
8340
  const usd = priceUsd(servedBy, usage);
@@ -8311,8 +8372,9 @@ function invoiceFromJournal(entries, priceUsd) {
8311
8372
  usage: record.usage,
8312
8373
  ...record.usageApprox === true ? { usageApprox: true } : {},
8313
8374
  ...usd === void 0 ? {} : { usd },
8375
+ allocatedUsd: 0,
8314
8376
  ...mark,
8315
- reconciliation: record.responseId !== void 0 ? "matched" : record.outcome === "ok" ? "missing-provider-id" : "unconfirmed"
8377
+ reconciliation: record.responseId !== void 0 ? "provider-id-present" : record.outcome === "ok" ? "missing-provider-id" : "unconfirmed"
8316
8378
  });
8317
8379
  }
8318
8380
  if (records.length === 0) {
@@ -8327,6 +8389,7 @@ function invoiceFromJournal(entries, priceUsd) {
8327
8389
  usage: slice.usage,
8328
8390
  ...entry.usageApprox === true ? { usageApprox: true } : {},
8329
8391
  ...usd === void 0 ? {} : { usd },
8392
+ allocatedUsd: 0,
8330
8393
  ...mark,
8331
8394
  reconciliation: "unattributed"
8332
8395
  });
@@ -8344,19 +8407,23 @@ function invoiceFromJournal(entries, priceUsd) {
8344
8407
  usage: remainder,
8345
8408
  ...entry.usageApprox === true ? { usageApprox: true } : {},
8346
8409
  ...usd === void 0 ? {} : { usd },
8410
+ allocatedUsd: 0,
8347
8411
  ...mark,
8348
8412
  reconciliation: "unattributed"
8349
8413
  });
8350
8414
  }
8351
8415
  }
8416
+ allocateRows(rows, entries, priceUsd, report.grossUsd);
8352
8417
  const usageApprox = report.usageApprox === true || report.abandoned.usageApprox === true;
8353
8418
  return {
8354
8419
  rows,
8355
8420
  totalUsd: report.grossUsd,
8356
8421
  netUsd: report.totalUsd,
8357
8422
  abandonedUsd: report.abandoned.usd,
8423
+ pricingBasis: "per-call",
8424
+ rowUsdNonAdditive: true,
8358
8425
  unpriced: [...report.unpriced, ...report.abandoned.unpriced],
8359
- reconciliationFailures: rows.filter((row) => row.reconciliation !== "matched").length,
8426
+ reconciliationFailures: rows.filter((row) => row.reconciliation !== "provider-id-present").length,
8360
8427
  ...usageApprox ? { usageApprox: true } : {}
8361
8428
  };
8362
8429
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.66.0",
3
+ "version": "1.67.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",