@rayadesu/dsh-llm-billing 0.3.11 → 0.3.13
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/README.i18n.yaml +2 -2
- package/README.md +17 -4
- package/README.zh.md +17 -4
- package/lib/index.js +302 -60
- package/lib/typert.host.js +79 -6
- package/lib/typert.remote-client.d.ts +3 -1
- package/lib/typert.remote-client.js +79 -6
- package/lib/types/balance.d.ts +22 -2
- package/lib/types/balance.js +19 -1
- package/lib/types/index.d.ts +2 -2
- package/lib/types/index.js +24 -5
- package/lib/types/today-spend.d.ts +192 -47
- package/lib/types/today-spend.js +270 -58
- package/lib/types/types.d.ts +50 -3
- package/package.json +20 -20
package/lib/index.js
CHANGED
|
@@ -136,6 +136,7 @@ let DeepSeekBalanceGateway = (() => {
|
|
|
136
136
|
let _getSessionSpend_decorators;
|
|
137
137
|
let _getTodaySpend_decorators;
|
|
138
138
|
let _getTodaySessionsSpend_decorators;
|
|
139
|
+
let _getDelegatedSpend_decorators;
|
|
139
140
|
let _getTurnSpend_decorators;
|
|
140
141
|
let _getSessionTurnSpends_decorators;
|
|
141
142
|
return class DeepSeekBalanceGateway extends _classSuper {
|
|
@@ -145,6 +146,7 @@ let DeepSeekBalanceGateway = (() => {
|
|
|
145
146
|
_getSessionSpend_decorators = [Remote("getSessionSpend")];
|
|
146
147
|
_getTodaySpend_decorators = [Remote("getTodaySpend")];
|
|
147
148
|
_getTodaySessionsSpend_decorators = [Remote("getTodaySessionsSpend")];
|
|
149
|
+
_getDelegatedSpend_decorators = [Remote("getDelegatedSpend")];
|
|
148
150
|
_getTurnSpend_decorators = [Remote("getTurnSpend")];
|
|
149
151
|
_getSessionTurnSpends_decorators = [Remote("getSessionTurnSpends")];
|
|
150
152
|
__esDecorate(this, null, _getBalance_decorators, {
|
|
@@ -191,6 +193,17 @@ let DeepSeekBalanceGateway = (() => {
|
|
|
191
193
|
},
|
|
192
194
|
metadata: _metadata
|
|
193
195
|
}, null, _instanceExtraInitializers);
|
|
196
|
+
__esDecorate(this, null, _getDelegatedSpend_decorators, {
|
|
197
|
+
kind: "method",
|
|
198
|
+
name: "getDelegatedSpend",
|
|
199
|
+
static: false,
|
|
200
|
+
private: false,
|
|
201
|
+
access: {
|
|
202
|
+
has: (obj) => "getDelegatedSpend" in obj,
|
|
203
|
+
get: (obj) => obj.getDelegatedSpend
|
|
204
|
+
},
|
|
205
|
+
metadata: _metadata
|
|
206
|
+
}, null, _instanceExtraInitializers);
|
|
194
207
|
__esDecorate(this, null, _getTurnSpend_decorators, {
|
|
195
208
|
kind: "method",
|
|
196
209
|
name: "getTurnSpend",
|
|
@@ -263,7 +276,9 @@ let DeepSeekBalanceGateway = (() => {
|
|
|
263
276
|
/**
|
|
264
277
|
* Read today's billed spend per session, restricted to the queried Beijing
|
|
265
278
|
* day. Rows carry the session's durable title and sort by cost descending;
|
|
266
|
-
* sessions with no priced usage on the day are omitted
|
|
279
|
+
* sessions with no priced usage on the day are omitted, and every subagent
|
|
280
|
+
* session is merged into the row of the top-level session that delegated it,
|
|
281
|
+
* so the ranking lists conversations rather than delegations.
|
|
267
282
|
* @param force - bypass the host-side 60s cache (manual refresh); omitted
|
|
268
283
|
* means a cached read.
|
|
269
284
|
* @returns today's per-session rows, highest first.
|
|
@@ -272,6 +287,19 @@ let DeepSeekBalanceGateway = (() => {
|
|
|
272
287
|
return this.options.fetchTodaySessionsSpend(force ?? false);
|
|
273
288
|
}
|
|
274
289
|
/**
|
|
290
|
+
* Read the subagent part of one conversation's billed spend: every subagent
|
|
291
|
+
* session the given session delegated, transitively, across every day (a
|
|
292
|
+
* session's own log cannot price its delegation children). `isSubagent`
|
|
293
|
+
* reports whether the queried session is itself a delegated child.
|
|
294
|
+
* @param sessionId - the session whose delegated subtree to sum.
|
|
295
|
+
* @param force - bypass the host-side 60s cache (manual refresh); omitted
|
|
296
|
+
* means a cached read.
|
|
297
|
+
* @returns the delegated subtotal plus its per-model rows.
|
|
298
|
+
*/
|
|
299
|
+
getDelegatedSpend(sessionId, force) {
|
|
300
|
+
return this.options.fetchDelegatedSpend(sessionId, force ?? false);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
275
303
|
* Read one completed Turn's billed spend (same per-event pricing as
|
|
276
304
|
* {@link priceEvent}).
|
|
277
305
|
* @param sessionId - the session owning the Turn.
|
|
@@ -1298,13 +1326,23 @@ function foldOwnBilling(unit, events, seedLength = 0) {
|
|
|
1298
1326
|
* every scan.
|
|
1299
1327
|
* - events path (plans A2/A3): collect and price only today's events in one
|
|
1300
1328
|
* pass (per-event Beijing-day filter during collection) with a hard cap,
|
|
1301
|
-
*
|
|
1302
|
-
*
|
|
1329
|
+
* adopting the fold it already priced for a session whose persisted revision
|
|
1330
|
+
* is unchanged since the last pass.
|
|
1303
1331
|
*
|
|
1304
1332
|
* Both strategies run behind the same {@link TodaySpendCache}, so a miss
|
|
1305
1333
|
* happens at most once per 60 seconds per process, and a manual refresh
|
|
1306
1334
|
* (`force`) bypasses the time window but keeps the revision caches — an
|
|
1307
|
-
* unchanged log provably cannot change the aggregate.
|
|
1335
|
+
* unchanged log provably cannot change the aggregate. That proof is what makes
|
|
1336
|
+
* a revision gate a CACHE: both strategies therefore adopt the resolution they
|
|
1337
|
+
* remember for an unchanged revision instead of skipping the session, so an
|
|
1338
|
+
* unchanged log costs no I/O and still contributes its full spend (and title)
|
|
1339
|
+
* to the aggregate and the ranking on every scan.
|
|
1340
|
+
*
|
|
1341
|
+
* The per-session ranking is a per-CONVERSATION ranking: a subagent child is
|
|
1342
|
+
* work the delegating conversation paid for, not a session the user opened, so
|
|
1343
|
+
* every subagent row is folded into the row of the top-level session at the
|
|
1344
|
+
* root of its `parentSession` chain (see {@link rollUpSubagentSpend}). The
|
|
1345
|
+
* aggregate is unaffected — it sums the same sessions either way.
|
|
1308
1346
|
*
|
|
1309
1347
|
* Forked sessions never double-count: a fork child's log opens with a
|
|
1310
1348
|
* verbatim copy of its source session's events (its inherited boundary), so
|
|
@@ -1344,6 +1382,79 @@ function foldSessionTitle(events) {
|
|
|
1344
1382
|
return null;
|
|
1345
1383
|
}
|
|
1346
1384
|
/**
|
|
1385
|
+
* Whether one session's durable header marks it as a subagent child. Either
|
|
1386
|
+
* marker is enough: `origin` is DSH's navigation classification and
|
|
1387
|
+
* `delegationDepth` is its persisted recursion budget, so a header carrying
|
|
1388
|
+
* only the depth (or only the origin) is still a delegation child. A session
|
|
1389
|
+
* created without either — an ordinary session, a user fork, or a cold resume
|
|
1390
|
+
* — is top-level.
|
|
1391
|
+
* @param header - the session's lineage slice; `undefined` reads as top-level.
|
|
1392
|
+
* @returns true when the session was created as a subagent child.
|
|
1393
|
+
*/
|
|
1394
|
+
function isSubagentSession(header) {
|
|
1395
|
+
if (header === void 0) return false;
|
|
1396
|
+
return header.origin === "subagent" || (header.delegationDepth ?? 0) > 0;
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* The top-level session one session's ranking row belongs to: the session
|
|
1400
|
+
* itself for a top-level session, and for a subagent child the first ancestor
|
|
1401
|
+
* up the `parentSession` chain that is not itself a subagent child. A
|
|
1402
|
+
* multi-generation delegation (a subagent that spawned subagents) therefore
|
|
1403
|
+
* lands on the same root row as its parent, and a child whose parent header is
|
|
1404
|
+
* unknown is attributed to the parent id its own header names — the parent is
|
|
1405
|
+
* authoritative even when its log is not part of this scan.
|
|
1406
|
+
* @param id - the session whose row is being attributed.
|
|
1407
|
+
* @param lineage - lineage of every session this scan saw, by id.
|
|
1408
|
+
* @returns the session id whose ranking row the input belongs to.
|
|
1409
|
+
*/
|
|
1410
|
+
function topLevelSessionOf(id, lineage) {
|
|
1411
|
+
let current = id;
|
|
1412
|
+
const seen = /* @__PURE__ */ new Set([current]);
|
|
1413
|
+
for (;;) {
|
|
1414
|
+
const header = lineage.get(current);
|
|
1415
|
+
if (!isSubagentSession(header)) return current;
|
|
1416
|
+
const parent = header?.parentSession;
|
|
1417
|
+
if (parent === void 0 || seen.has(parent)) return current;
|
|
1418
|
+
seen.add(parent);
|
|
1419
|
+
current = parent;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Fold every subagent child's row into the top-level row it belongs to
|
|
1424
|
+
* ({@link topLevelSessionOf}), so the ranking lists conversations rather than
|
|
1425
|
+
* every delegation a conversation started. A child's spend is added to its
|
|
1426
|
+
* ancestor's `total`; the ancestor's `ownTotal` keeps its own spend only. A
|
|
1427
|
+
* top-level session whose own day was empty but whose subagents priced
|
|
1428
|
+
* something still gets a row (with `ownTotal` 0), carrying the title the scan
|
|
1429
|
+
* resolved for it in `titles`.
|
|
1430
|
+
* @param rows - one row per session that priced something today (own spends).
|
|
1431
|
+
* @param lineage - lineage of every session this scan saw, by id.
|
|
1432
|
+
* @param titles - resolved display titles by session id; a session absent from
|
|
1433
|
+
* the map has no resolved title and its created row reports `null`.
|
|
1434
|
+
* @returns the merged rows, sorted by `total` descending.
|
|
1435
|
+
*/
|
|
1436
|
+
function rollUpSubagentSpend(rows, lineage, titles = /* @__PURE__ */ new Map()) {
|
|
1437
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1438
|
+
for (const row of rows) {
|
|
1439
|
+
const target = topLevelSessionOf(row.sessionId, lineage);
|
|
1440
|
+
const carried = merged.get(target);
|
|
1441
|
+
if (carried === void 0) {
|
|
1442
|
+
merged.set(target, target === row.sessionId ? row : {
|
|
1443
|
+
sessionId: target,
|
|
1444
|
+
title: titles.get(target) ?? null,
|
|
1445
|
+
total: row.total,
|
|
1446
|
+
ownTotal: 0
|
|
1447
|
+
});
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
merged.set(target, {
|
|
1451
|
+
...carried,
|
|
1452
|
+
total: carried.total + row.total
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
return [...merged.values()].sort((left, right) => right.total - left.total);
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1347
1458
|
* Read one live session's complete event log across both runtime families.
|
|
1348
1459
|
* @throws when the session exposes neither the legacy `events` snapshot nor
|
|
1349
1460
|
* the newer `snapshotEvents()` reader — an unknown runtime surface must
|
|
@@ -1484,23 +1595,78 @@ function evictOldest$1(map, limit) {
|
|
|
1484
1595
|
if (oldest !== void 0) map.delete(oldest);
|
|
1485
1596
|
}
|
|
1486
1597
|
/**
|
|
1598
|
+
* The merged whole-session spend of every subagent session delegated FROM one
|
|
1599
|
+
* session, transitively: the subagent subtotal a conversation's own log cannot
|
|
1600
|
+
* price. Only sessions DSH marked as delegation children count, so a user fork
|
|
1601
|
+
* (which names a `parentSession` too) is never billed into its source. A
|
|
1602
|
+
* malformed lineage that points back at the queried session is ignored rather
|
|
1603
|
+
* than counted twice.
|
|
1604
|
+
* @param id - the session whose delegated subtree to sum.
|
|
1605
|
+
* @param ownSpend - whole-session own spend per session, from one scan pass.
|
|
1606
|
+
* @param lineage - lineage per session, from the same pass.
|
|
1607
|
+
* @returns the merged subtree spend; empty when the session delegated nothing priced.
|
|
1608
|
+
*/
|
|
1609
|
+
function delegatedSpendOf(id, ownSpend, lineage) {
|
|
1610
|
+
const children = /* @__PURE__ */ new Map();
|
|
1611
|
+
for (const [childId, header] of lineage) {
|
|
1612
|
+
const parent = header.parentSession;
|
|
1613
|
+
if (parent === void 0 || !isSubagentSession(header)) continue;
|
|
1614
|
+
const siblings = children.get(parent);
|
|
1615
|
+
if (siblings === void 0) children.set(parent, [childId]);
|
|
1616
|
+
else siblings.push(childId);
|
|
1617
|
+
}
|
|
1618
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
1619
|
+
const pending = [...children.get(id) ?? []];
|
|
1620
|
+
for (const child of pending) seen.add(child);
|
|
1621
|
+
let total = emptyTodaySpend();
|
|
1622
|
+
while (pending.length > 0) {
|
|
1623
|
+
const current = pending.pop();
|
|
1624
|
+
total = mergeTodaySpend(total, ownSpend.get(current) ?? emptyTodaySpend());
|
|
1625
|
+
for (const child of children.get(current) ?? []) {
|
|
1626
|
+
if (seen.has(child)) continue;
|
|
1627
|
+
seen.add(child);
|
|
1628
|
+
pending.push(child);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return total;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1487
1634
|
* The aggregate computation behind a cache miss. Chooses the projection path
|
|
1488
1635
|
* when the projection registry is composed, the events path otherwise; both
|
|
1489
|
-
* gate cold reads on persisted revisions
|
|
1490
|
-
* sessions whose logs
|
|
1636
|
+
* gate cold reads on persisted revisions AND adopt the fold they already hold
|
|
1637
|
+
* for an unchanged log, so steady-state scans re-read only sessions whose logs
|
|
1638
|
+
* actually changed while every unchanged session keeps contributing.
|
|
1491
1639
|
*/
|
|
1492
1640
|
var TodaySpendScanner = class {
|
|
1493
1641
|
deps;
|
|
1494
|
-
/**
|
|
1642
|
+
/**
|
|
1643
|
+
* Cold sessions resolved by either strategy: id → the persisted revision the
|
|
1644
|
+
* resolution saw, the session's OWN-events fold, and its folded title.
|
|
1645
|
+
*
|
|
1646
|
+
* The two strategies differ in how they PRICE a cold log (an eager projection
|
|
1647
|
+
* cell plus the cache ladder, or a local fold over the read log), never in
|
|
1648
|
+
* what an unchanged log contributes to the day — so one memory serves both.
|
|
1649
|
+
* The projection path reuses the resolved unit; the events path reuses the
|
|
1650
|
+
* fold it priced on the previous pass. A strategy that skipped an unchanged
|
|
1651
|
+
* log WITHOUT adopting its remembered fold would silently drop that session
|
|
1652
|
+
* from the aggregate and the ranking on every scan after the first.
|
|
1653
|
+
*/
|
|
1495
1654
|
coldResolved = /* @__PURE__ */ new Map();
|
|
1496
1655
|
/** Cold sessions whose resolution failed: id → revision (retried only when the log changes). */
|
|
1497
1656
|
coldFailed = /* @__PURE__ */ new Map();
|
|
1498
|
-
/** Cold sessions resolved on the events path: id → revision (events were collected). */
|
|
1499
|
-
lastEventsScan;
|
|
1500
1657
|
constructor(deps) {
|
|
1501
1658
|
this.deps = deps;
|
|
1502
1659
|
}
|
|
1503
1660
|
/**
|
|
1661
|
+
* Remember one cold session's resolution, bounded by
|
|
1662
|
+
* {@link COLD_RESOLVE_CACHE_LIMIT}: evicting the oldest entry (instead of
|
|
1663
|
+
* clearing) keeps the other sessions' resolved state warm across scans.
|
|
1664
|
+
*/
|
|
1665
|
+
rememberCold(id, resolution) {
|
|
1666
|
+
evictOldest$1(this.coldResolved, COLD_RESOLVE_CACHE_LIMIT);
|
|
1667
|
+
this.coldResolved.set(id, resolution);
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1504
1670
|
* Compute today's aggregate for one Beijing day.
|
|
1505
1671
|
* @param dayKey - the Beijing-time calendar-day key to aggregate.
|
|
1506
1672
|
* @returns today's spend across every session.
|
|
@@ -1510,8 +1676,11 @@ var TodaySpendScanner = class {
|
|
|
1510
1676
|
}
|
|
1511
1677
|
/**
|
|
1512
1678
|
* Compute today's per-session spend for one Beijing day, sorted by cost
|
|
1513
|
-
* descending.
|
|
1514
|
-
*
|
|
1679
|
+
* descending. One row per top-level session: sessions with no priced usage
|
|
1680
|
+
* on the day are omitted (unless their subagents priced something, which the
|
|
1681
|
+
* roll-up merges into their row), every subagent session is folded into the
|
|
1682
|
+
* top-level session that delegated it, and each row carries the session's
|
|
1683
|
+
* durable title folded from its log.
|
|
1515
1684
|
* @param dayKey - the Beijing-time calendar-day key to aggregate.
|
|
1516
1685
|
* @returns today's per-session rows, highest first.
|
|
1517
1686
|
*/
|
|
@@ -1524,6 +1693,9 @@ var TodaySpendScanner = class {
|
|
|
1524
1693
|
* read, unit fold, and title fold instead of scanning twice. Chooses the
|
|
1525
1694
|
* projection path when the projection registry is composed, the events path
|
|
1526
1695
|
* otherwise.
|
|
1696
|
+
*
|
|
1697
|
+
* The aggregate sums every priced session, subagents included — the ranking's
|
|
1698
|
+
* subagent roll-up only regroups rows, so neither total moves.
|
|
1527
1699
|
* @param dayKey - the Beijing-time calendar-day key to aggregate.
|
|
1528
1700
|
* @returns the aggregate plus per-session rows sorted by cost descending.
|
|
1529
1701
|
*/
|
|
@@ -1533,7 +1705,7 @@ var TodaySpendScanner = class {
|
|
|
1533
1705
|
return this.scanDetailProjections(dayKey);
|
|
1534
1706
|
}
|
|
1535
1707
|
/**
|
|
1536
|
-
* Resolve one cold session's billing
|
|
1708
|
+
* Resolve one cold session's billing fold state and display title.
|
|
1537
1709
|
*
|
|
1538
1710
|
* The zero-I/O projection-cache row answers the query directly whenever its
|
|
1539
1711
|
* own latest priced day is NOT the queried day: the row then proves the
|
|
@@ -1551,7 +1723,7 @@ var TodaySpendScanner = class {
|
|
|
1551
1723
|
* @param header - the listed session header (the cache identity witness).
|
|
1552
1724
|
* @param seeded - whether the session carries a fork-inherited prefix.
|
|
1553
1725
|
* @param dayKey - the Beijing-time day being aggregated.
|
|
1554
|
-
* @returns the resolved state and title, or `undefined` when unreadable.
|
|
1726
|
+
* @returns the resolved fold state and title, or `undefined` when unreadable.
|
|
1555
1727
|
*/
|
|
1556
1728
|
async resolveCold(header, seeded, dayKey) {
|
|
1557
1729
|
const { persistence, projectionCache, logger } = this.deps;
|
|
@@ -1560,7 +1732,7 @@ var TodaySpendScanner = class {
|
|
|
1560
1732
|
if (cache !== void 0) try {
|
|
1561
1733
|
const value = cache.cachedSnapshot(header, 0, [BILLING_UNIT_KEY])?.values[BILLING_UNIT_KEY];
|
|
1562
1734
|
if (value !== void 0 && value.dayKey !== dayKey) return {
|
|
1563
|
-
value,
|
|
1735
|
+
fold: value,
|
|
1564
1736
|
title: null
|
|
1565
1737
|
};
|
|
1566
1738
|
} catch (error) {
|
|
@@ -1572,7 +1744,7 @@ var TodaySpendScanner = class {
|
|
|
1572
1744
|
try {
|
|
1573
1745
|
const read = await persistenceInspect(persistenceService, header.id);
|
|
1574
1746
|
return {
|
|
1575
|
-
|
|
1747
|
+
fold: foldOwnBilling(this.deps.unit, read.events, read.seedLength),
|
|
1576
1748
|
title: foldSessionTitle(read.events)
|
|
1577
1749
|
};
|
|
1578
1750
|
} catch (error) {
|
|
@@ -1595,11 +1767,11 @@ var TodaySpendScanner = class {
|
|
|
1595
1767
|
/**
|
|
1596
1768
|
* Cold-ladder adopt: for every stored session not live, either the
|
|
1597
1769
|
* revision-gated resolution already in {@link coldResolved} is adopted
|
|
1598
|
-
* (unchanged log costs nothing) or the session is queued
|
|
1599
|
-
* parallel fan-out, resolved, remembered, and then adopted.
|
|
1600
|
-
* resolution failed is remembered too (by revision), so an
|
|
1601
|
-
* is not re-read on every scan; a changed revision retries it.
|
|
1602
|
-
* unreadable session never blanks the whole-day aggregate.
|
|
1770
|
+
* (unchanged log costs nothing and still counts) or the session is queued
|
|
1771
|
+
* behind a bounded parallel fan-out, resolved, remembered, and then adopted.
|
|
1772
|
+
* A session whose resolution failed is remembered too (by revision), so an
|
|
1773
|
+
* unreadable log is not re-read on every scan; a changed revision retries it.
|
|
1774
|
+
* One unreadable session never blanks the whole-day aggregate.
|
|
1603
1775
|
* @param liveIds - ids of sessions already folded from the live store.
|
|
1604
1776
|
* @param snapshots - stored snapshot list (either runtime family).
|
|
1605
1777
|
* @param dayKey - the Beijing-time day being aggregated.
|
|
@@ -1627,8 +1799,7 @@ var TodaySpendScanner = class {
|
|
|
1627
1799
|
const resolved = await this.resolveCold(header, seeded, dayKey);
|
|
1628
1800
|
if (resolved !== void 0) {
|
|
1629
1801
|
this.coldFailed.delete(header.id);
|
|
1630
|
-
|
|
1631
|
-
this.coldResolved.set(header.id, {
|
|
1802
|
+
this.rememberCold(header.id, {
|
|
1632
1803
|
revision,
|
|
1633
1804
|
...resolved
|
|
1634
1805
|
});
|
|
@@ -1645,14 +1816,17 @@ var TodaySpendScanner = class {
|
|
|
1645
1816
|
/**
|
|
1646
1817
|
* Events-path collection shared by both aggregate and per-session scans:
|
|
1647
1818
|
* fold each session's log with the shared pricing fold (attempt samples with
|
|
1648
|
-
* same-step replacement) and announce the session's latest-day spend
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
1819
|
+
* same-step replacement) and announce the session's latest-day spend. A
|
|
1820
|
+
* persisted session whose log did not change since it was last resolved is
|
|
1821
|
+
* answered from {@link coldResolved} instead of being re-read: it keeps
|
|
1822
|
+
* counting toward the aggregate and the ranking at zero cost, which is what
|
|
1823
|
+
* makes the revision gate a cache rather than a way to lose sessions. A fork
|
|
1824
|
+
* child's inherited prefix (`seq < seedLength`) is skipped, so each model
|
|
1825
|
+
* output is priced only in its source session. The hard cap counts the
|
|
1826
|
+
* queried day's events; a truncated pass remembers nothing it read, so the
|
|
1827
|
+
* next one re-reads whatever this one cut short.
|
|
1654
1828
|
* @param dayKey - the Beijing-time calendar-day key to aggregate.
|
|
1655
|
-
* @param onSession -
|
|
1829
|
+
* @param onSession - adopt one session's fold, title, and lineage.
|
|
1656
1830
|
* @returns whether the hard cap truncated the scan.
|
|
1657
1831
|
*/
|
|
1658
1832
|
async collectTodayEvents(dayKey, onSession) {
|
|
@@ -1660,7 +1834,8 @@ var TodaySpendScanner = class {
|
|
|
1660
1834
|
const liveIds = /* @__PURE__ */ new Set();
|
|
1661
1835
|
let collected = 0;
|
|
1662
1836
|
let truncated = false;
|
|
1663
|
-
|
|
1837
|
+
/** Price one complete log, announce it, and return what to remember. */
|
|
1838
|
+
const collect = (id, events, seedLength, lineage) => {
|
|
1664
1839
|
const folder = new BillingFolder(billing, catalog, seedLength);
|
|
1665
1840
|
for (const event of events) {
|
|
1666
1841
|
if (beijingPartsOf(event.time).dayKey === dayKey) {
|
|
@@ -1672,13 +1847,19 @@ var TodaySpendScanner = class {
|
|
|
1672
1847
|
}
|
|
1673
1848
|
folder.add(event);
|
|
1674
1849
|
}
|
|
1675
|
-
|
|
1850
|
+
const fold = folder.fold;
|
|
1851
|
+
const title = foldSessionTitle(events);
|
|
1852
|
+
onSession(id, fold, title, lineage);
|
|
1853
|
+
return {
|
|
1854
|
+
fold,
|
|
1855
|
+
title
|
|
1856
|
+
};
|
|
1676
1857
|
};
|
|
1677
1858
|
if (sessions !== void 0) {
|
|
1678
1859
|
const store = sessions();
|
|
1679
1860
|
if (store !== void 0) for (const session of store.list()) {
|
|
1680
1861
|
liveIds.add(session.id);
|
|
1681
|
-
collect(session.id, liveSessionEvents(session), forkBoundaryOf(session));
|
|
1862
|
+
collect(session.id, liveSessionEvents(session), forkBoundaryOf(session), session.header ?? {});
|
|
1682
1863
|
if (truncated) break;
|
|
1683
1864
|
}
|
|
1684
1865
|
}
|
|
@@ -1687,16 +1868,23 @@ var TodaySpendScanner = class {
|
|
|
1687
1868
|
const snapshots = await persistenceListSnapshots(persistenceService);
|
|
1688
1869
|
for (const { header, revision } of snapshots) {
|
|
1689
1870
|
if (liveIds.has(header.id)) continue;
|
|
1690
|
-
|
|
1871
|
+
const resolved = this.coldResolved.get(header.id);
|
|
1872
|
+
if (resolved !== void 0 && resolved.revision === revision) {
|
|
1873
|
+
onSession(header.id, resolved.fold, resolved.title, header);
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1691
1876
|
try {
|
|
1692
1877
|
const read = await persistenceInspect(persistenceService, header.id);
|
|
1693
|
-
collect(header.id, read.events, read.seedLength);
|
|
1878
|
+
const priced = collect(header.id, read.events, read.seedLength, header);
|
|
1879
|
+
if (!truncated) this.rememberCold(header.id, {
|
|
1880
|
+
revision,
|
|
1881
|
+
...priced
|
|
1882
|
+
});
|
|
1694
1883
|
} catch (error) {
|
|
1695
1884
|
logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
|
|
1696
1885
|
}
|
|
1697
1886
|
if (truncated) break;
|
|
1698
1887
|
}
|
|
1699
|
-
if (!truncated) this.lastEventsScan = new Map(snapshots.map((snapshot) => [snapshot.header.id, snapshot.revision]));
|
|
1700
1888
|
}
|
|
1701
1889
|
if (truncated) logger.warn(`llm-billing: today's events exceeded ${maxEvents}; result truncated`);
|
|
1702
1890
|
return truncated;
|
|
@@ -1706,7 +1894,9 @@ var TodaySpendScanner = class {
|
|
|
1706
1894
|
* (title folded from the live log, so a rename is reflected immediately),
|
|
1707
1895
|
* revision-gated cold ladder for the rest (title resolved on inspect, `null`
|
|
1708
1896
|
* when answered from the projection cache). A fork child's cell covers its
|
|
1709
|
-
* inherited prefix, so its own-events fold supplies both outputs.
|
|
1897
|
+
* inherited prefix, so its own-events fold supplies both outputs. Lineage
|
|
1898
|
+
* (which session delegated which) comes from the same headers the boundary
|
|
1899
|
+
* does, so the ranking's subagent roll-up costs no extra read.
|
|
1710
1900
|
* @param dayKey - the Beijing-time calendar-day key to aggregate.
|
|
1711
1901
|
* @returns the aggregate plus per-session rows, sorted by cost descending.
|
|
1712
1902
|
*/
|
|
@@ -1715,71 +1905,107 @@ var TodaySpendScanner = class {
|
|
|
1715
1905
|
const projectionsService = projections?.();
|
|
1716
1906
|
let aggregate = emptyTodaySpend();
|
|
1717
1907
|
const rows = /* @__PURE__ */ new Map();
|
|
1908
|
+
const lineage = /* @__PURE__ */ new Map();
|
|
1909
|
+
const titles = /* @__PURE__ */ new Map();
|
|
1910
|
+
const ownSpend = /* @__PURE__ */ new Map();
|
|
1911
|
+
const createdAt = /* @__PURE__ */ new Map();
|
|
1718
1912
|
const liveIds = /* @__PURE__ */ new Set();
|
|
1719
1913
|
if (sessions !== void 0) {
|
|
1720
1914
|
const store = sessions();
|
|
1721
1915
|
if (store !== void 0) for (const { session, state } of this.liveBillingEntries(store, projectionsService)) {
|
|
1722
1916
|
liveIds.add(session.id);
|
|
1723
|
-
|
|
1917
|
+
lineage.set(session.id, session.header ?? {});
|
|
1918
|
+
const born = session.header?.createdAt;
|
|
1919
|
+
if (born !== void 0) createdAt.set(session.id, born);
|
|
1920
|
+
const title = foldSessionTitle(liveSessionEvents(session));
|
|
1921
|
+
titles.set(session.id, title);
|
|
1922
|
+
if (state === void 0) continue;
|
|
1923
|
+
ownSpend.set(session.id, state.session);
|
|
1924
|
+
if (state.dayKey !== dayKey) continue;
|
|
1724
1925
|
aggregate = mergeTodaySpend(aggregate, state.spend);
|
|
1725
1926
|
rows.set(session.id, {
|
|
1726
1927
|
sessionId: session.id,
|
|
1727
|
-
title
|
|
1728
|
-
total: state.spend.total
|
|
1928
|
+
title,
|
|
1929
|
+
total: state.spend.total,
|
|
1930
|
+
ownTotal: state.spend.total
|
|
1729
1931
|
});
|
|
1730
1932
|
}
|
|
1731
1933
|
}
|
|
1732
1934
|
const persistenceService = persistence?.();
|
|
1733
1935
|
if (persistenceService !== void 0) {
|
|
1734
1936
|
const snapshots = await persistenceListSnapshots(persistenceService);
|
|
1937
|
+
for (const { header } of snapshots) {
|
|
1938
|
+
if (liveIds.has(header.id)) continue;
|
|
1939
|
+
lineage.set(header.id, header);
|
|
1940
|
+
if (header.createdAt !== void 0) createdAt.set(header.id, header.createdAt);
|
|
1941
|
+
}
|
|
1735
1942
|
await this.coldAdopt(liveIds, snapshots, dayKey, (id, resolved) => {
|
|
1736
|
-
if (resolved.
|
|
1737
|
-
|
|
1943
|
+
if (resolved.title !== null && !titles.has(id)) titles.set(id, resolved.title);
|
|
1944
|
+
ownSpend.set(id, resolved.fold.session);
|
|
1945
|
+
if (resolved.fold.dayKey !== dayKey) return;
|
|
1946
|
+
aggregate = mergeTodaySpend(aggregate, resolved.fold.spend);
|
|
1738
1947
|
rows.set(id, {
|
|
1739
1948
|
sessionId: id,
|
|
1740
1949
|
title: resolved.title,
|
|
1741
|
-
total: resolved.
|
|
1950
|
+
total: resolved.fold.spend.total,
|
|
1951
|
+
ownTotal: resolved.fold.spend.total
|
|
1742
1952
|
});
|
|
1743
1953
|
});
|
|
1744
1954
|
}
|
|
1745
1955
|
return {
|
|
1746
1956
|
aggregate,
|
|
1747
|
-
sessions:
|
|
1957
|
+
sessions: rollUpSubagentSpend([...rows.values()], lineage, titles),
|
|
1958
|
+
dayKey,
|
|
1959
|
+
ownSpend,
|
|
1960
|
+
lineage,
|
|
1961
|
+
createdAt
|
|
1748
1962
|
};
|
|
1749
1963
|
}
|
|
1750
1964
|
/**
|
|
1751
1965
|
* Events path, one pass for both outputs: price today's events (per-event
|
|
1752
|
-
* Beijing-day filter during collection, hard cap), gated
|
|
1966
|
+
* Beijing-day filter during collection, hard cap), revision-gated so an
|
|
1967
|
+
* unchanged log is adopted from {@link coldResolved} instead of re-read. A
|
|
1753
1968
|
* fork child's inherited prefix (`seq < seedLength`) is skipped, so each
|
|
1754
1969
|
* model output is priced only in its source session. Titles fold from each
|
|
1755
1970
|
* session's complete log — a `session/title` event can predate today — so a
|
|
1756
|
-
* rename is reflected as soon as the session's log is re-read
|
|
1971
|
+
* rename is reflected as soon as the session's log is re-read, and an
|
|
1972
|
+
* unchanged session keeps the title its earlier read folded. Lineage comes
|
|
1973
|
+
* from the same headers, which the ranking roll-up needs.
|
|
1757
1974
|
* @param dayKey - the Beijing-time calendar-day key to aggregate.
|
|
1758
1975
|
* @returns the aggregate plus per-session rows, sorted by cost descending.
|
|
1759
1976
|
*/
|
|
1760
1977
|
async scanDetailEvents(dayKey) {
|
|
1761
1978
|
let aggregate = emptyTodaySpend();
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1979
|
+
const rows = [];
|
|
1980
|
+
const lineage = /* @__PURE__ */ new Map();
|
|
1981
|
+
const titles = /* @__PURE__ */ new Map();
|
|
1982
|
+
const ownSpend = /* @__PURE__ */ new Map();
|
|
1983
|
+
const createdAt = /* @__PURE__ */ new Map();
|
|
1984
|
+
await this.collectTodayEvents(dayKey, (id, fold, title, sessionLineage) => {
|
|
1985
|
+
lineage.set(id, sessionLineage);
|
|
1986
|
+
titles.set(id, title);
|
|
1987
|
+
const born = sessionLineage.createdAt;
|
|
1988
|
+
if (born !== void 0) createdAt.set(id, born);
|
|
1989
|
+
ownSpend.set(id, fold.session);
|
|
1764
1990
|
if (fold.dayKey !== dayKey) return;
|
|
1765
1991
|
aggregate = mergeTodaySpend(aggregate, fold.spend);
|
|
1766
|
-
|
|
1992
|
+
rows.push({
|
|
1767
1993
|
sessionId: id,
|
|
1768
|
-
title
|
|
1769
|
-
total: fold.spend.total
|
|
1994
|
+
title,
|
|
1995
|
+
total: fold.spend.total,
|
|
1996
|
+
ownTotal: fold.spend.total
|
|
1770
1997
|
});
|
|
1771
1998
|
});
|
|
1772
|
-
sessions.sort((left, right) => right.total - left.total);
|
|
1773
1999
|
return {
|
|
1774
2000
|
aggregate,
|
|
1775
|
-
sessions
|
|
2001
|
+
sessions: rollUpSubagentSpend(rows, lineage, titles),
|
|
2002
|
+
dayKey,
|
|
2003
|
+
ownSpend,
|
|
2004
|
+
lineage,
|
|
2005
|
+
createdAt
|
|
1776
2006
|
};
|
|
1777
2007
|
}
|
|
1778
2008
|
};
|
|
1779
|
-
/** Per-session rows from the map, highest total first. */
|
|
1780
|
-
function sortRows(rows) {
|
|
1781
|
-
return [...rows.values()].sort((left, right) => right.total - left.total);
|
|
1782
|
-
}
|
|
1783
2009
|
//#endregion
|
|
1784
2010
|
//#region lib/types/index.js
|
|
1785
2011
|
/**
|
|
@@ -2021,11 +2247,15 @@ function createUnitRegistrar(ctx, unit) {
|
|
|
2021
2247
|
* events path serves today's spend.
|
|
2022
2248
|
* - plans A1–A3: the scanner chooses the projection path when the registry
|
|
2023
2249
|
* is composed, the events path otherwise.
|
|
2250
|
+
* All three reads come from ONE cached pass: the day's aggregate, its
|
|
2251
|
+
* per-session ranking, and the delegated-subagent subtree of a conversation
|
|
2252
|
+
* (the same pass folds each session's whole-session total, so the subtree read
|
|
2253
|
+
* costs no second scan).
|
|
2024
2254
|
* @param ctx - plugin context.
|
|
2025
2255
|
* @param facts - resolved endpoint, credential, pricing, and catalog facts.
|
|
2026
2256
|
* @param unit - the shared projection unit definition.
|
|
2027
2257
|
* @param ensureUnit - idempotent unit registrar (last-resort registration).
|
|
2028
|
-
* @returns the
|
|
2258
|
+
* @returns the three today-spend loaders.
|
|
2029
2259
|
*/
|
|
2030
2260
|
function createTodaySpendLoaders(ctx, facts, unit, ensureUnit) {
|
|
2031
2261
|
const scanner = new TodaySpendScanner({
|
|
@@ -2043,7 +2273,18 @@ function createTodaySpendLoaders(ctx, facts, unit, ensureUnit) {
|
|
|
2043
2273
|
const todayCache = new TodaySpendCache((dayKey) => scanner.scanDetail(dayKey), TODAY_SPEND_CACHE_MS);
|
|
2044
2274
|
return {
|
|
2045
2275
|
fetchTodaySpend: async (force = false) => (await todayCache.get(force)).aggregate,
|
|
2046
|
-
fetchTodaySessionsSpend: async (force = false) => ({ sessions: (await todayCache.get(force)).sessions })
|
|
2276
|
+
fetchTodaySessionsSpend: async (force = false) => ({ sessions: (await todayCache.get(force)).sessions }),
|
|
2277
|
+
fetchDelegatedSpend: async (sessionId, force = false) => {
|
|
2278
|
+
const detail = await todayCache.get(force);
|
|
2279
|
+
const spend = delegatedSpendOf(sessionId, detail.ownSpend, detail.lineage);
|
|
2280
|
+
const createdAt = detail.createdAt.get(sessionId);
|
|
2281
|
+
return {
|
|
2282
|
+
total: spend.total,
|
|
2283
|
+
models: spend.models,
|
|
2284
|
+
isSubagent: isSubagentSession(detail.lineage.get(sessionId)),
|
|
2285
|
+
crossedDay: createdAt !== void 0 && beijingDayKey(new Date(createdAt)) !== detail.dayKey
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2047
2288
|
};
|
|
2048
2289
|
}
|
|
2049
2290
|
/** One completed Turn's spend loader, located by its closing message id. */
|
|
@@ -2128,7 +2369,7 @@ function apply(ctx, config) {
|
|
|
2128
2369
|
ctx.on("session/created", ensureUnit);
|
|
2129
2370
|
const fetchBalance = createBalanceFetcher(ctx, facts);
|
|
2130
2371
|
const fetchSessionSpend = createSessionSpendFetcher(ctx, facts);
|
|
2131
|
-
const { fetchTodaySpend, fetchTodaySessionsSpend } = createTodaySpendLoaders(ctx, facts, unit, ensureUnit);
|
|
2372
|
+
const { fetchTodaySpend, fetchTodaySessionsSpend, fetchDelegatedSpend } = createTodaySpendLoaders(ctx, facts, unit, ensureUnit);
|
|
2132
2373
|
const fetchTurnSpend = createTurnSpendFetcher(ctx, facts);
|
|
2133
2374
|
const fetchTurnSpends = createTurnSpendsFetcher(ctx, facts);
|
|
2134
2375
|
new DeepSeekBalanceGateway(ctx, {
|
|
@@ -2136,9 +2377,10 @@ function apply(ctx, config) {
|
|
|
2136
2377
|
fetchSessionSpend,
|
|
2137
2378
|
fetchTodaySpend,
|
|
2138
2379
|
fetchTodaySessionsSpend,
|
|
2380
|
+
fetchDelegatedSpend,
|
|
2139
2381
|
fetchTurnSpend,
|
|
2140
2382
|
fetchTurnSpends
|
|
2141
2383
|
});
|
|
2142
2384
|
}
|
|
2143
2385
|
//#endregion
|
|
2144
|
-
export { BALANCE_CACHE_MS, BALANCE_TIMEOUT_MS, BILLING_UNIT_KEY, BillingFolder, Config, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, DeepSeekBalanceGateway, FLASH_SERIES_RATE_CHANGE_AT, PUBLIC_BASE_URL, SESSION_SPEND_CACHE_LIMIT, SESSION_TURN_SPEND_CACHE_LIMIT, SessionTurnSpendFolder, SpendAccumulator, TODAY_SPEND_CACHE_MS, TODAY_SPEND_MAX_EVENTS, TodaySpendCache, TodaySpendScanner, V4_PRO_ROUTE_SWITCH_AT, addEventContribution, apply, applyBillingEvent, beijingDayKey, billingTodaySpendDefinition, computeSessionSpend, computeSessionTurnSpends, computeTodaySpend, computeTurnSpend, emptyBillingFoldState, emptyTodaySpend, fetchDeepSeekBalance, foldBillingUnit, foldOwnBilling, foldSessionTitle, forkBoundaryOf, isPeak, isSeededSession, liveSessionEvents, mergeTodaySpend, name, negateSpend, parseDeepSeekBalance, persistenceInspect, persistenceListSnapshots, priceEvent, priceUsage, resolveBilling, subtractSpend };
|
|
2386
|
+
export { BALANCE_CACHE_MS, BALANCE_TIMEOUT_MS, BILLING_UNIT_KEY, BillingFolder, Config, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, DeepSeekBalanceGateway, FLASH_SERIES_RATE_CHANGE_AT, PUBLIC_BASE_URL, SESSION_SPEND_CACHE_LIMIT, SESSION_TURN_SPEND_CACHE_LIMIT, SessionTurnSpendFolder, SpendAccumulator, TODAY_SPEND_CACHE_MS, TODAY_SPEND_MAX_EVENTS, TodaySpendCache, TodaySpendScanner, V4_PRO_ROUTE_SWITCH_AT, addEventContribution, apply, applyBillingEvent, beijingDayKey, billingTodaySpendDefinition, computeSessionSpend, computeSessionTurnSpends, computeTodaySpend, computeTurnSpend, delegatedSpendOf, emptyBillingFoldState, emptyTodaySpend, fetchDeepSeekBalance, foldBillingUnit, foldOwnBilling, foldSessionTitle, forkBoundaryOf, isPeak, isSeededSession, isSubagentSession, liveSessionEvents, mergeTodaySpend, name, negateSpend, parseDeepSeekBalance, persistenceInspect, persistenceListSnapshots, priceEvent, priceUsage, resolveBilling, rollUpSubagentSpend, subtractSpend, topLevelSessionOf };
|