@rayadesu/dsh-llm-billing 0.3.4 → 0.3.6
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 +5 -1
- package/README.zh.md +5 -1
- package/lib/index.js +82 -48
- package/lib/types/billing.d.ts +43 -12
- package/lib/types/billing.js +24 -12
- package/lib/types/index.d.ts +2 -2
- package/lib/types/index.js +11 -7
- package/lib/types/today-spend.d.ts +50 -16
- package/lib/types/today-spend.js +61 -32
- package/package.json +1 -1
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# last confirmed-consistent state. Both languages carry equal authority; after
|
|
3
3
|
# editing either side, bring the other along and re-record both hashes with:
|
|
4
4
|
# git hash-object README.md README.zh.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 85c6cc052cd3332f23351db8123259565a669602
|
|
6
|
+
README.zh.md: 96d0d9d70fe94bfb0afa2e2af3f97231103d3590
|
package/README.md
CHANGED
|
@@ -31,7 +31,11 @@ Note: the projection path prices a session's history once, at the rates in effec
|
|
|
31
31
|
|
|
32
32
|
## Forked sessions
|
|
33
33
|
|
|
34
|
-
A forked session (DSH's "fork" of a conversation) opens its log with a verbatim copy of its source session's events. Without special handling, the same model outputs would be billed once per copy: the child's session spend would include the inherited prefix, and today's spend would count it a second time alongside the parent's. The plugin prices only a session's OWN events — the
|
|
34
|
+
A forked session (DSH's "fork" of a conversation) opens its log with a verbatim copy of its source session's events. Without special handling, the same model outputs would be billed once per copy: the child's session spend would include the inherited prefix, and today's spend would count it a second time alongside the parent's. The plugin prices only a session's OWN events — the fork boundary is the session's persisted state (`header.seedLength` on the ≤ 0.1.1-rc.2 runtime, `Session.inheritedEventCount` / `inspect().inheritedEventCount` on 0.1.2-alpha.4+, both read structurally), and every event with `seq < boundary` is treated as already billed in the source session. Fork children are therefore billed from their first new exchange onward (a freshly forked session prices to zero), today's spend counts each model output exactly once, and the same lineage-safe rule covers multi-generation forks and subagent forks (spawned with `context: 'fork'`). The boundary is the persisted value, so a resumed fork child keeps its original boundary, while a session created without a seed — ordinary sessions and cold resumes included — carries no boundary and is billed in full.
|
|
35
|
+
|
|
36
|
+
## Runtime compatibility
|
|
37
|
+
|
|
38
|
+
Since 0.1.2-alpha.4, DSH replaced the live `Session` log surface `Session.events` with `Session.snapshotEvents()` (no args = the full current log) and `Session.ownEvents()`, and moved `SessionHeader.seedLength` to `Session.inheritedEventCount` (the persistence `inspect()` result carries the value beside `meta`; `listSnapshots()` headers keep only the boolean `isSeeded`). Every log read in the plugin goes through the structural adapters `liveSessionEvents` / `forkBoundaryOf` / `isSeededSession`, which accept both the ≤ 0.1.1-rc.2 and the 0.1.2-alpha.4+ shapes — the npm release baseline (`^0.1.1-rc.2`) and the ahead-of-npm monorepo runtime both work without modification. On an unknown surface that has neither shape the plugin fails loudly rather than silently pricing an empty log.
|
|
35
39
|
|
|
36
40
|
## Configuration
|
|
37
41
|
|
package/README.zh.md
CHANGED
|
@@ -31,7 +31,11 @@
|
|
|
31
31
|
|
|
32
32
|
## 分叉会话
|
|
33
33
|
|
|
34
|
-
分叉会话(DSH
|
|
34
|
+
分叉会话(DSH 的「分叉会话」)的日志以来源会话事件的逐字节副本开头。若不特殊处理,同一批模型输出会按副本数重复计费:子会话的会话花费会包含继承前缀,今日花费也会在父会话之外再计一次。插件只对会话的**自有事件**计费——分叉边界取自已持久化的会话状态(≤ 0.1.1-rc.2 运行时为 `header.seedLength`;0.1.2-alpha.4+ 运行时为 `Session.inheritedEventCount` / `inspect().inheritedEventCount`,两者都以结构方式读取),凡是 `seq < 边界` 的事件都视为已在来源会话计费。因此分叉子会话从分叉后的第一次新交流开始计费(刚分叉的会话花费为零),今日花费对每个模型输出只计一次,同一血缘规则同样覆盖多代分叉与 subagent 分叉(`context: 'fork'` 生成)。边界取自已持久化的值,所以恢复后的分叉子会话保持原边界;而创建时没有 seed 的会话——包括普通会话与冷恢复——不带边界,正常全额计费。
|
|
35
|
+
|
|
36
|
+
## 运行时兼容性
|
|
37
|
+
|
|
38
|
+
0.1.2-alpha.4 起,DSH 把 live `Session` 的日志读取表面从 `Session.events` 改为 `Session.snapshotEvents()`(无参 = 当前全量日志)与 `Session.ownEvents()`,并把 `SessionHeader.seedLength` 移至 `Session.inheritedEventCount`(持久化侧 `inspect()` 的结果在 `meta` 之外携带该值,`listSnapshots()` 的 header 只剩布尔 `isSeeded`)。插件的所有日志读取都走结构适配器 `liveSessionEvents` / `forkBoundaryOf` / `isSeededSession`,同时接受 ≤ 0.1.1-rc.2 与 0.1.2-alpha.4+ 两种形状——npm 发布基线(`^0.1.1-rc.2`)与超前于它的 monorepo 运行时代码均无需改动即可工作。遇到两种形状都没有的未知运行时表面时,插件会显式失败而不是静默按零花费计价。
|
|
35
39
|
|
|
36
40
|
## 配置
|
|
37
41
|
|
package/lib/index.js
CHANGED
|
@@ -380,20 +380,28 @@ function beijingDayKey(now) {
|
|
|
380
380
|
return beijingParts(now.getTime()).dayKey;
|
|
381
381
|
}
|
|
382
382
|
/**
|
|
383
|
-
* The durable inherited-prefix boundary of one session:
|
|
384
|
-
*
|
|
385
|
-
* (or any seeded replay) carries
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* @param header - the session's durable header, when available.
|
|
383
|
+
* The durable inherited-prefix boundary of one session: the number of leading
|
|
384
|
+
* events it inherited verbatim from its fork source, 0 for a session created
|
|
385
|
+
* without a seed. A forked session (or any seeded replay) carries that count
|
|
386
|
+
* in its session state; every event with `seq < seedLength` is a copy of an
|
|
387
|
+
* event already billed in that source session, so pricing must skip them or
|
|
388
|
+
* the same model output is counted once per copy. Accepts the durable field
|
|
389
|
+
* of both DSH runtime families (see {@link ForkBoundarySource}).
|
|
390
|
+
* @param source - the session, inspection result, header slice, or durable
|
|
391
|
+
* header carrying the boundary; `undefined` reads as 0.
|
|
393
392
|
* @returns the inherited-prefix length; 0 for an unseeded session.
|
|
394
393
|
*/
|
|
395
|
-
function forkBoundaryOf(
|
|
396
|
-
|
|
394
|
+
function forkBoundaryOf(source) {
|
|
395
|
+
if (source === void 0) return 0;
|
|
396
|
+
const inherited = source.inheritedEventCount;
|
|
397
|
+
if (inherited !== void 0 && Number.isSafeInteger(inherited)) return inherited;
|
|
398
|
+
return source.header?.seedLength ?? source.meta?.seedLength ?? source.seedLength ?? 0;
|
|
399
|
+
}
|
|
400
|
+
/** Whether a durable header marks a fork-inherited (seeded) session across both runtime families. */
|
|
401
|
+
function isSeededSession(header) {
|
|
402
|
+
if (header === void 0) return false;
|
|
403
|
+
if (header.isSeeded === true) return true;
|
|
404
|
+
return (header.seedLength ?? 0) > 0;
|
|
397
405
|
}
|
|
398
406
|
/** Whether a Beijing (hour, weekday) pair falls inside any peak-hour window. */
|
|
399
407
|
function isPeakParts(billing, hour, weekday) {
|
|
@@ -782,14 +790,22 @@ function foldOwnBilling(unit, events, seedLength = 0) {
|
|
|
782
790
|
* unchanged log provably cannot change the aggregate.
|
|
783
791
|
*
|
|
784
792
|
* Forked sessions never double-count: a fork child's log opens with a
|
|
785
|
-
* verbatim copy of its source session's events (
|
|
786
|
-
* so the scanner prices only the child's OWN events
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
* its original boundary and an unseeded session stays at 0.
|
|
793
|
+
* verbatim copy of its source session's events (its inherited boundary),
|
|
794
|
+
* so the scanner prices only the child's OWN events on every path — the
|
|
795
|
+
* projection path bypasses the eager cell for a seeded session and folds its
|
|
796
|
+
* own events instead (the cell covers the inherited prefix too), and the cold
|
|
797
|
+
* ladder skips the projection cache for a seeded session (its cached row
|
|
798
|
+
* predates the boundary and covers inherited events). The boundary is the
|
|
799
|
+
* durable session state, read across both DSH runtime families — a resumed
|
|
800
|
+
* fork child keeps its original boundary and an unseeded session stays at 0.
|
|
801
|
+
*
|
|
802
|
+
* The live `Session` log surface changed in 0.1.2-alpha.4: `Session.events`
|
|
803
|
+
* was removed and replaced by `Session.snapshotEvents()` / `ownEvents()`, and
|
|
804
|
+
* `SessionHeader.seedLength` moved to `Session.inheritedEventCount` (the
|
|
805
|
+
* persistence `inspect` result carries it alongside `meta`). Reads go through
|
|
806
|
+
* {@link liveSessionEvents} / {@link forkBoundaryOf}, which accept both
|
|
807
|
+
* families structurally, so the scanner runs on the ≤ 0.1.1-rc.2 npm baseline
|
|
808
|
+
* and on the newer runtime.
|
|
793
809
|
* @module @rayadesu/dsh-llm-billing/today-spend
|
|
794
810
|
*/
|
|
795
811
|
/**
|
|
@@ -810,6 +826,17 @@ function foldSessionTitle(events) {
|
|
|
810
826
|
return null;
|
|
811
827
|
}
|
|
812
828
|
/**
|
|
829
|
+
* Read one live session's complete event log across both runtime families.
|
|
830
|
+
* @throws when the session exposes neither the legacy `events` snapshot nor
|
|
831
|
+
* the newer `snapshotEvents()` reader — an unknown runtime surface must
|
|
832
|
+
* fail loudly rather than silently price an empty log.
|
|
833
|
+
*/
|
|
834
|
+
function liveSessionEvents(session) {
|
|
835
|
+
if (session.events !== void 0) return session.events;
|
|
836
|
+
if (session.snapshotEvents !== void 0) return session.snapshotEvents();
|
|
837
|
+
throw new Error("llm-billing: session log surface is neither Session.events nor Session.snapshotEvents");
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
813
840
|
* Bounded parallel fan-out: run `run` over `items` with at most `limit` in
|
|
814
841
|
* flight. A shared index counter hands each worker its next job, so the
|
|
815
842
|
* dispatch is O(n) overall (array `shift()` would be O(n) per pop).
|
|
@@ -923,16 +950,19 @@ var TodaySpendScanner = class {
|
|
|
923
950
|
* until the session is inspected again. A SEEDED session (fork child)
|
|
924
951
|
* skips the ladder entirely: its cached row was folded over the inherited
|
|
925
952
|
* prefix too, so it always detaches through inspect with the durable
|
|
926
|
-
* boundary
|
|
953
|
+
* boundary (the inspect result's inherited count or `meta.seedLength`,
|
|
954
|
+
* depending on the runtime family) applied to the local fold.
|
|
927
955
|
* @param id - the cold session's id.
|
|
928
|
-
* @param
|
|
956
|
+
* @param seeded - whether the session carries a fork-inherited prefix
|
|
957
|
+
* (from the snapshot header: `isSeeded` on 0.1.2-alpha.4+, `seedLength`
|
|
958
|
+
* at and before the 0.1.1-rc.2 baseline).
|
|
929
959
|
* @returns the resolved state and title, or `undefined` when unreadable.
|
|
930
960
|
*/
|
|
931
|
-
async resolveCold(id,
|
|
932
|
-
const { persistence, projectionCache,
|
|
961
|
+
async resolveCold(id, seeded) {
|
|
962
|
+
const { persistence, projectionCache, logger } = this.deps;
|
|
933
963
|
const persistenceService = persistence?.();
|
|
934
964
|
if (persistenceService === void 0) return void 0;
|
|
935
|
-
if (
|
|
965
|
+
if (!seeded) {
|
|
936
966
|
const cache = projectionCache?.();
|
|
937
967
|
if (cache !== void 0) try {
|
|
938
968
|
const value = (await cache.coldSnapshot(id)).values[BILLING_UNIT_KEY];
|
|
@@ -947,7 +977,7 @@ var TodaySpendScanner = class {
|
|
|
947
977
|
try {
|
|
948
978
|
const inspection = await persistenceService.inspect(id);
|
|
949
979
|
return {
|
|
950
|
-
value: foldOwnBilling(unit, inspection.events,
|
|
980
|
+
value: foldOwnBilling(this.deps.unit, inspection.events, forkBoundaryOf(inspection)),
|
|
951
981
|
title: foldSessionTitle(inspection.events)
|
|
952
982
|
};
|
|
953
983
|
} catch (error) {
|
|
@@ -992,8 +1022,8 @@ var TodaySpendScanner = class {
|
|
|
992
1022
|
const store = sessions();
|
|
993
1023
|
if (store !== void 0) for (const session of store.list()) {
|
|
994
1024
|
liveIds.add(session.id);
|
|
995
|
-
const seedLength = forkBoundaryOf(session
|
|
996
|
-
const state = seedLength > 0 ? this.ownBillingState(session.id, session
|
|
1025
|
+
const seedLength = forkBoundaryOf(session);
|
|
1026
|
+
const state = seedLength > 0 ? this.ownBillingState(session.id, liveSessionEvents(session), seedLength) : projectionsService?.stateOf(session, BILLING_UNIT_KEY);
|
|
997
1027
|
if (state !== void 0 && state.dayKey === dayKey) total = mergeTodaySpend(total, state.spend);
|
|
998
1028
|
}
|
|
999
1029
|
}
|
|
@@ -1003,7 +1033,7 @@ var TodaySpendScanner = class {
|
|
|
1003
1033
|
const pending = [];
|
|
1004
1034
|
for (const { header, revision } of snapshots) {
|
|
1005
1035
|
if (liveIds.has(header.id)) continue;
|
|
1006
|
-
const
|
|
1036
|
+
const seeded = isSeededSession(header);
|
|
1007
1037
|
const resolved = this.coldResolved.get(header.id);
|
|
1008
1038
|
if (resolved !== void 0 && resolved.revision === revision) {
|
|
1009
1039
|
if (resolved.value.dayKey === dayKey) total = mergeTodaySpend(total, resolved.value.spend);
|
|
@@ -1012,11 +1042,11 @@ var TodaySpendScanner = class {
|
|
|
1012
1042
|
pending.push({
|
|
1013
1043
|
id: header.id,
|
|
1014
1044
|
revision,
|
|
1015
|
-
|
|
1045
|
+
seeded
|
|
1016
1046
|
});
|
|
1017
1047
|
}
|
|
1018
|
-
await withConcurrency(pending, 8, async ({ id, revision,
|
|
1019
|
-
const resolved = await this.resolveCold(id,
|
|
1048
|
+
await withConcurrency(pending, 8, async ({ id, revision, seeded }) => {
|
|
1049
|
+
const resolved = await this.resolveCold(id, seeded);
|
|
1020
1050
|
if (resolved !== void 0) this.coldResolved.set(id, {
|
|
1021
1051
|
revision,
|
|
1022
1052
|
...resolved
|
|
@@ -1058,7 +1088,7 @@ var TodaySpendScanner = class {
|
|
|
1058
1088
|
const store = sessions();
|
|
1059
1089
|
if (store !== void 0) for (const session of store.list()) {
|
|
1060
1090
|
liveIds.add(session.id);
|
|
1061
|
-
collect(session
|
|
1091
|
+
collect(liveSessionEvents(session), forkBoundaryOf(session));
|
|
1062
1092
|
if (truncated) break;
|
|
1063
1093
|
}
|
|
1064
1094
|
}
|
|
@@ -1070,7 +1100,7 @@ var TodaySpendScanner = class {
|
|
|
1070
1100
|
if (this.lastEventsScan?.get(header.id) === revision) continue;
|
|
1071
1101
|
try {
|
|
1072
1102
|
const inspection = await persistenceService.inspect(header.id);
|
|
1073
|
-
collect(inspection.events, forkBoundaryOf(inspection
|
|
1103
|
+
collect(inspection.events, forkBoundaryOf(inspection));
|
|
1074
1104
|
} catch (error) {
|
|
1075
1105
|
logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
|
|
1076
1106
|
}
|
|
@@ -1099,11 +1129,12 @@ var TodaySpendScanner = class {
|
|
|
1099
1129
|
const store = sessions();
|
|
1100
1130
|
if (store !== void 0) for (const session of store.list()) {
|
|
1101
1131
|
liveIds.add(session.id);
|
|
1102
|
-
const seedLength = forkBoundaryOf(session
|
|
1103
|
-
const
|
|
1132
|
+
const seedLength = forkBoundaryOf(session);
|
|
1133
|
+
const events = liveSessionEvents(session);
|
|
1134
|
+
const state = seedLength > 0 ? this.ownBillingState(session.id, events, seedLength) : projectionsService?.stateOf(session, BILLING_UNIT_KEY);
|
|
1104
1135
|
if (state !== void 0 && state.dayKey === dayKey) rows.set(session.id, {
|
|
1105
1136
|
sessionId: session.id,
|
|
1106
|
-
title: foldSessionTitle(
|
|
1137
|
+
title: foldSessionTitle(events),
|
|
1107
1138
|
total: state.spend.total
|
|
1108
1139
|
});
|
|
1109
1140
|
}
|
|
@@ -1114,7 +1145,7 @@ var TodaySpendScanner = class {
|
|
|
1114
1145
|
const pending = [];
|
|
1115
1146
|
for (const { header, revision } of snapshots) {
|
|
1116
1147
|
if (liveIds.has(header.id)) continue;
|
|
1117
|
-
const
|
|
1148
|
+
const seeded = isSeededSession(header);
|
|
1118
1149
|
const resolved = this.coldResolved.get(header.id);
|
|
1119
1150
|
if (resolved !== void 0 && resolved.revision === revision) {
|
|
1120
1151
|
if (resolved.value.dayKey === dayKey) rows.set(header.id, {
|
|
@@ -1127,11 +1158,11 @@ var TodaySpendScanner = class {
|
|
|
1127
1158
|
pending.push({
|
|
1128
1159
|
id: header.id,
|
|
1129
1160
|
revision,
|
|
1130
|
-
|
|
1161
|
+
seeded
|
|
1131
1162
|
});
|
|
1132
1163
|
}
|
|
1133
|
-
await withConcurrency(pending, 8, async ({ id, revision,
|
|
1134
|
-
const resolved = await this.resolveCold(id,
|
|
1164
|
+
await withConcurrency(pending, 8, async ({ id, revision, seeded }) => {
|
|
1165
|
+
const resolved = await this.resolveCold(id, seeded);
|
|
1135
1166
|
if (resolved !== void 0) this.coldResolved.set(id, {
|
|
1136
1167
|
revision,
|
|
1137
1168
|
...resolved
|
|
@@ -1190,7 +1221,7 @@ var TodaySpendScanner = class {
|
|
|
1190
1221
|
const store = sessions();
|
|
1191
1222
|
if (store !== void 0) for (const session of store.list()) {
|
|
1192
1223
|
liveIds.add(session.id);
|
|
1193
|
-
collect(session.id, session
|
|
1224
|
+
collect(session.id, liveSessionEvents(session), forkBoundaryOf(session));
|
|
1194
1225
|
if (truncated) break;
|
|
1195
1226
|
}
|
|
1196
1227
|
}
|
|
@@ -1202,7 +1233,7 @@ var TodaySpendScanner = class {
|
|
|
1202
1233
|
if (this.lastEventsScan?.get(header.id) === revision) continue;
|
|
1203
1234
|
try {
|
|
1204
1235
|
const inspection = await persistenceService.inspect(header.id);
|
|
1205
|
-
collect(header.id, inspection.events, forkBoundaryOf(inspection
|
|
1236
|
+
collect(header.id, inspection.events, forkBoundaryOf(inspection));
|
|
1206
1237
|
} catch (error) {
|
|
1207
1238
|
logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
|
|
1208
1239
|
}
|
|
@@ -1294,7 +1325,10 @@ const TODAY_SPEND_MAX_EVENTS = 2e5;
|
|
|
1294
1325
|
/**
|
|
1295
1326
|
* Read one session's event log and durable seed boundary: the live
|
|
1296
1327
|
* SessionStore first, then the persistence backend for a flushed session
|
|
1297
|
-
* (inspected directly by id — no header listing).
|
|
1328
|
+
* (inspected directly by id — no header listing). The live-session surface
|
|
1329
|
+
* is read structurally across both DSH runtime families — `Session.events`
|
|
1330
|
+
* (≤ 0.1.1-rc.2) or `Session.snapshotEvents()` + `Session.inheritedEventCount`
|
|
1331
|
+
* (0.1.2-alpha.4+) — via {@link liveSessionEvents} / {@link forkBoundaryOf}.
|
|
1298
1332
|
* @param ctx - plugin context carrying the SessionStore and optional persistence.
|
|
1299
1333
|
* @param sessionId - the session to read.
|
|
1300
1334
|
* @returns the session's complete event log plus its inherited-prefix boundary.
|
|
@@ -1303,15 +1337,15 @@ const TODAY_SPEND_MAX_EVENTS = 2e5;
|
|
|
1303
1337
|
async function sessionEvents(ctx, sessionId) {
|
|
1304
1338
|
const live = ctx.get("sessions")?.get(sessionId);
|
|
1305
1339
|
if (live !== void 0) return {
|
|
1306
|
-
events: live
|
|
1307
|
-
seedLength: forkBoundaryOf(live
|
|
1340
|
+
events: liveSessionEvents(live),
|
|
1341
|
+
seedLength: forkBoundaryOf(live)
|
|
1308
1342
|
};
|
|
1309
1343
|
const persistence = ctx.get("sessionPersistence");
|
|
1310
1344
|
if (persistence !== void 0) try {
|
|
1311
1345
|
const inspection = await persistence.inspect(sessionId);
|
|
1312
1346
|
return {
|
|
1313
1347
|
events: inspection.events,
|
|
1314
|
-
seedLength: forkBoundaryOf(inspection
|
|
1348
|
+
seedLength: forkBoundaryOf(inspection)
|
|
1315
1349
|
};
|
|
1316
1350
|
} catch (error) {
|
|
1317
1351
|
throw new LlmError(`llm-billing: session ${sessionId} not found`, "NOT_FOUND", { cause: error });
|
|
@@ -1406,4 +1440,4 @@ function apply(ctx, config) {
|
|
|
1406
1440
|
});
|
|
1407
1441
|
}
|
|
1408
1442
|
//#endregion
|
|
1409
|
-
export { BILLING_UNIT_KEY, Config, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, DeepSeekBalanceGateway, PUBLIC_BASE_URL, SpendAccumulator, TODAY_SPEND_CACHE_MS, TODAY_SPEND_MAX_EVENTS, TodaySpendCache, TodaySpendScanner, addEventContribution, apply, beijingDayKey, billingTodaySpendDefinition, computeSessionSpend, computeTodaySpend, computeTurnSpend, emptyTodaySpend, fetchDeepSeekBalance, foldBillingUnit, foldOwnBilling, foldSessionTitle, forkBoundaryOf, isPeak, mergeTodaySpend, name, parseDeepSeekBalance, priceEvent, resolveBilling };
|
|
1443
|
+
export { BILLING_UNIT_KEY, Config, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, DeepSeekBalanceGateway, PUBLIC_BASE_URL, SpendAccumulator, TODAY_SPEND_CACHE_MS, TODAY_SPEND_MAX_EVENTS, TodaySpendCache, TodaySpendScanner, addEventContribution, apply, beijingDayKey, billingTodaySpendDefinition, computeSessionSpend, computeTodaySpend, computeTurnSpend, emptyTodaySpend, fetchDeepSeekBalance, foldBillingUnit, foldOwnBilling, foldSessionTitle, forkBoundaryOf, isPeak, isSeededSession, liveSessionEvents, mergeTodaySpend, name, parseDeepSeekBalance, priceEvent, resolveBilling };
|
package/lib/types/billing.d.ts
CHANGED
|
@@ -88,21 +88,52 @@ export declare function resolveBilling(config: BillingConfig | undefined): Resol
|
|
|
88
88
|
/** The Beijing (Asia/Shanghai, UTC+8, no DST) calendar-day key of a timestamp. */
|
|
89
89
|
export declare function beijingDayKey(now: Date): string;
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
91
|
+
* Structural source of a session's durable inherited-prefix boundary. The
|
|
92
|
+
* field moved between DSH runtimes:
|
|
93
|
+
*
|
|
94
|
+
* - since 0.1.2-alpha.4, `SessionHeader.seedLength` was removed (the header
|
|
95
|
+
* now carries only `isSeeded`: boolean) and the exact cut moved to
|
|
96
|
+
* `Session.inheritedEventCount` / `SessionInspection.inheritedEventCount`;
|
|
97
|
+
* - at and before the 0.1.1-rc.2 npm baseline, the cut lived on the durable
|
|
98
|
+
* header as `seedLength` (optional; absent for an unseeded session) and on
|
|
99
|
+
* the persistence `inspect` result as `meta.seedLength`.
|
|
100
|
+
*
|
|
101
|
+
* The reader accepts any of the three shapes and prefers the newer,
|
|
102
|
+
* exact-count field, so the same plugin code prices correctly on both
|
|
103
|
+
* families of runtime.
|
|
104
|
+
*/
|
|
105
|
+
export interface ForkBoundarySource {
|
|
106
|
+
/** 0.1.2-alpha.4+: `Session.inheritedEventCount` / `SessionInspection.inheritedEventCount`. */
|
|
107
|
+
readonly inheritedEventCount?: number;
|
|
108
|
+
/** 0.1.1-rc.2 and earlier: `SessionHeader.seedLength`. */
|
|
109
|
+
readonly seedLength?: number;
|
|
110
|
+
/** ≤ 0.1.1-rc.2: the header slice of a `Session` (its `header.seedLength`). */
|
|
111
|
+
readonly header?: {
|
|
112
|
+
readonly seedLength?: number;
|
|
113
|
+
};
|
|
114
|
+
/** ≤ 0.1.1-rc.2: the `meta` of a persistence `inspect` result (its `seedLength`). */
|
|
115
|
+
readonly meta?: {
|
|
116
|
+
readonly seedLength?: number;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The durable inherited-prefix boundary of one session: the number of leading
|
|
121
|
+
* events it inherited verbatim from its fork source, 0 for a session created
|
|
122
|
+
* without a seed. A forked session (or any seeded replay) carries that count
|
|
123
|
+
* in its session state; every event with `seq < seedLength` is a copy of an
|
|
124
|
+
* event already billed in that source session, so pricing must skip them or
|
|
125
|
+
* the same model output is counted once per copy. Accepts the durable field
|
|
126
|
+
* of both DSH runtime families (see {@link ForkBoundarySource}).
|
|
127
|
+
* @param source - the session, inspection result, header slice, or durable
|
|
128
|
+
* header carrying the boundary; `undefined` reads as 0.
|
|
101
129
|
* @returns the inherited-prefix length; 0 for an unseeded session.
|
|
102
130
|
*/
|
|
103
|
-
export declare function forkBoundaryOf(
|
|
131
|
+
export declare function forkBoundaryOf(source: ForkBoundarySource | undefined): number;
|
|
132
|
+
/** Whether a durable header marks a fork-inherited (seeded) session across both runtime families. */
|
|
133
|
+
export declare function isSeededSession(header: {
|
|
104
134
|
readonly seedLength?: number;
|
|
105
|
-
|
|
135
|
+
readonly isSeeded?: boolean;
|
|
136
|
+
} | undefined): boolean;
|
|
106
137
|
/**
|
|
107
138
|
* Whether a timestamp falls inside any peak-hour window (Beijing time,
|
|
108
139
|
* weekdays Monday–Friday only). Weekends (Saturday and Sunday) are always
|
package/lib/types/billing.js
CHANGED
|
@@ -81,20 +81,32 @@ export function beijingDayKey(now) {
|
|
|
81
81
|
return beijingParts(now.getTime()).dayKey;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* The durable inherited-prefix boundary of one session:
|
|
85
|
-
*
|
|
86
|
-
* (or any seeded replay) carries
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* @param header - the session's durable header, when available.
|
|
84
|
+
* The durable inherited-prefix boundary of one session: the number of leading
|
|
85
|
+
* events it inherited verbatim from its fork source, 0 for a session created
|
|
86
|
+
* without a seed. A forked session (or any seeded replay) carries that count
|
|
87
|
+
* in its session state; every event with `seq < seedLength` is a copy of an
|
|
88
|
+
* event already billed in that source session, so pricing must skip them or
|
|
89
|
+
* the same model output is counted once per copy. Accepts the durable field
|
|
90
|
+
* of both DSH runtime families (see {@link ForkBoundarySource}).
|
|
91
|
+
* @param source - the session, inspection result, header slice, or durable
|
|
92
|
+
* header carrying the boundary; `undefined` reads as 0.
|
|
94
93
|
* @returns the inherited-prefix length; 0 for an unseeded session.
|
|
95
94
|
*/
|
|
96
|
-
export function forkBoundaryOf(
|
|
97
|
-
|
|
95
|
+
export function forkBoundaryOf(source) {
|
|
96
|
+
if (source === undefined)
|
|
97
|
+
return 0;
|
|
98
|
+
const inherited = source.inheritedEventCount;
|
|
99
|
+
if (inherited !== undefined && Number.isSafeInteger(inherited))
|
|
100
|
+
return inherited;
|
|
101
|
+
return source.header?.seedLength ?? source.meta?.seedLength ?? source.seedLength ?? 0;
|
|
102
|
+
}
|
|
103
|
+
/** Whether a durable header marks a fork-inherited (seeded) session across both runtime families. */
|
|
104
|
+
export function isSeededSession(header) {
|
|
105
|
+
if (header === undefined)
|
|
106
|
+
return false;
|
|
107
|
+
if (header.isSeeded === true)
|
|
108
|
+
return true;
|
|
109
|
+
return (header.seedLength ?? 0) > 0;
|
|
98
110
|
}
|
|
99
111
|
/** Whether a Beijing (hour, weekday) pair falls inside any peak-hour window. */
|
|
100
112
|
function isPeakParts(billing, hour, weekday) {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -24,12 +24,12 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
24
24
|
import z from '@deepseek-ai/schemastery';
|
|
25
25
|
import type { BillingConfig } from './billing.ts';
|
|
26
26
|
export { DeepSeekBalanceGateway, fetchDeepSeekBalance, parseDeepSeekBalance } from './balance.ts';
|
|
27
|
-
export { addEventContribution, beijingDayKey, computeSessionSpend, computeTodaySpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyTodaySpend, forkBoundaryOf, isPeak, mergeTodaySpend, priceEvent, resolveBilling, SpendAccumulator, } from './billing.ts';
|
|
27
|
+
export { addEventContribution, beijingDayKey, computeSessionSpend, computeTodaySpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyTodaySpend, forkBoundaryOf, isPeak, isSeededSession, mergeTodaySpend, priceEvent, resolveBilling, SpendAccumulator, } from './billing.ts';
|
|
28
28
|
export type { BillingConfig, BillingConfigModel, BillingEventContribution, DeepSeekModelPricing, DeepSeekTokenPrice, PeakHourWindow, ResolvedBilling, } from './billing.ts';
|
|
29
29
|
export type * from './types.ts';
|
|
30
30
|
export { BILLING_UNIT_KEY, billingTodaySpendDefinition, foldBillingUnit, foldOwnBilling } from './projection.ts';
|
|
31
31
|
export type { BillingUnitState } from './projection.ts';
|
|
32
|
-
export { foldSessionTitle, TodaySpendCache, TodaySpendScanner } from './today-spend.ts';
|
|
32
|
+
export { foldSessionTitle, liveSessionEvents, TodaySpendCache, TodaySpendScanner } from './today-spend.ts';
|
|
33
33
|
export type { ScannerPersistedHeader, ScannerSession, TodaySpendScannerDeps } from './today-spend.ts';
|
|
34
34
|
export declare const name = "llm-billing";
|
|
35
35
|
/** Public API default; deployments may point elsewhere via $DEEPSEEK_BASE_URL. */
|
package/lib/types/index.js
CHANGED
|
@@ -27,11 +27,11 @@ import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
|
|
|
27
27
|
import { DeepSeekBalanceGateway, fetchDeepSeekBalance } from "./balance.js";
|
|
28
28
|
import { computeSessionSpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, forkBoundaryOf, mergeTodaySpend, resolveBilling, } from "./billing.js";
|
|
29
29
|
import { billingTodaySpendDefinition } from "./projection.js";
|
|
30
|
-
import { TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
|
|
30
|
+
import { liveSessionEvents, TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
|
|
31
31
|
export { DeepSeekBalanceGateway, fetchDeepSeekBalance, parseDeepSeekBalance } from "./balance.js";
|
|
32
|
-
export { addEventContribution, beijingDayKey, computeSessionSpend, computeTodaySpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyTodaySpend, forkBoundaryOf, isPeak, mergeTodaySpend, priceEvent, resolveBilling, SpendAccumulator, } from "./billing.js";
|
|
32
|
+
export { addEventContribution, beijingDayKey, computeSessionSpend, computeTodaySpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyTodaySpend, forkBoundaryOf, isPeak, isSeededSession, mergeTodaySpend, priceEvent, resolveBilling, SpendAccumulator, } from "./billing.js";
|
|
33
33
|
export { BILLING_UNIT_KEY, billingTodaySpendDefinition, foldBillingUnit, foldOwnBilling } from "./projection.js";
|
|
34
|
-
export { foldSessionTitle, TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
|
|
34
|
+
export { foldSessionTitle, liveSessionEvents, TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
|
|
35
35
|
export const name = 'llm-billing';
|
|
36
36
|
const DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY';
|
|
37
37
|
const BASE_URL_ENV = 'DEEPSEEK_BASE_URL';
|
|
@@ -75,7 +75,10 @@ export const TODAY_SPEND_MAX_EVENTS = 200_000;
|
|
|
75
75
|
/**
|
|
76
76
|
* Read one session's event log and durable seed boundary: the live
|
|
77
77
|
* SessionStore first, then the persistence backend for a flushed session
|
|
78
|
-
* (inspected directly by id — no header listing).
|
|
78
|
+
* (inspected directly by id — no header listing). The live-session surface
|
|
79
|
+
* is read structurally across both DSH runtime families — `Session.events`
|
|
80
|
+
* (≤ 0.1.1-rc.2) or `Session.snapshotEvents()` + `Session.inheritedEventCount`
|
|
81
|
+
* (0.1.2-alpha.4+) — via {@link liveSessionEvents} / {@link forkBoundaryOf}.
|
|
79
82
|
* @param ctx - plugin context carrying the SessionStore and optional persistence.
|
|
80
83
|
* @param sessionId - the session to read.
|
|
81
84
|
* @returns the session's complete event log plus its inherited-prefix boundary.
|
|
@@ -84,13 +87,14 @@ export const TODAY_SPEND_MAX_EVENTS = 200_000;
|
|
|
84
87
|
async function sessionEvents(ctx, sessionId) {
|
|
85
88
|
const sessions = ctx.get('sessions');
|
|
86
89
|
const live = sessions?.get(sessionId);
|
|
87
|
-
if (live !== undefined)
|
|
88
|
-
return { events: live
|
|
90
|
+
if (live !== undefined) {
|
|
91
|
+
return { events: liveSessionEvents(live), seedLength: forkBoundaryOf(live) };
|
|
92
|
+
}
|
|
89
93
|
const persistence = ctx.get('sessionPersistence');
|
|
90
94
|
if (persistence !== undefined) {
|
|
91
95
|
try {
|
|
92
96
|
const inspection = await persistence.inspect(sessionId);
|
|
93
|
-
return { events: inspection.events, seedLength: forkBoundaryOf(inspection
|
|
97
|
+
return { events: inspection.events, seedLength: forkBoundaryOf(inspection) };
|
|
94
98
|
}
|
|
95
99
|
catch (error) {
|
|
96
100
|
throw new LlmError(`llm-billing: session ${sessionId} not found`, 'NOT_FOUND', { cause: error });
|
|
@@ -20,14 +20,22 @@
|
|
|
20
20
|
* unchanged log provably cannot change the aggregate.
|
|
21
21
|
*
|
|
22
22
|
* Forked sessions never double-count: a fork child's log opens with a
|
|
23
|
-
* verbatim copy of its source session's events (
|
|
24
|
-
* so the scanner prices only the child's OWN events
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* its original boundary and an unseeded session stays at 0.
|
|
23
|
+
* verbatim copy of its source session's events (its inherited boundary),
|
|
24
|
+
* so the scanner prices only the child's OWN events on every path — the
|
|
25
|
+
* projection path bypasses the eager cell for a seeded session and folds its
|
|
26
|
+
* own events instead (the cell covers the inherited prefix too), and the cold
|
|
27
|
+
* ladder skips the projection cache for a seeded session (its cached row
|
|
28
|
+
* predates the boundary and covers inherited events). The boundary is the
|
|
29
|
+
* durable session state, read across both DSH runtime families — a resumed
|
|
30
|
+
* fork child keeps its original boundary and an unseeded session stays at 0.
|
|
31
|
+
*
|
|
32
|
+
* The live `Session` log surface changed in 0.1.2-alpha.4: `Session.events`
|
|
33
|
+
* was removed and replaced by `Session.snapshotEvents()` / `ownEvents()`, and
|
|
34
|
+
* `SessionHeader.seedLength` moved to `Session.inheritedEventCount` (the
|
|
35
|
+
* persistence `inspect` result carries it alongside `meta`). Reads go through
|
|
36
|
+
* {@link liveSessionEvents} / {@link forkBoundaryOf}, which accept both
|
|
37
|
+
* families structurally, so the scanner runs on the ≤ 0.1.1-rc.2 npm baseline
|
|
38
|
+
* and on the newer runtime.
|
|
31
39
|
* @module @rayadesu/dsh-llm-billing/today-spend
|
|
32
40
|
*/
|
|
33
41
|
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session';
|
|
@@ -45,20 +53,40 @@ import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
|
|
|
45
53
|
* @returns the session's current title, or `null` when untitled.
|
|
46
54
|
*/
|
|
47
55
|
export declare function foldSessionTitle(events: readonly SessionEvent[]): string | null;
|
|
48
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Structural slice of a live session the scanner reads, accepting both DSH
|
|
58
|
+
* runtime families: the ≤ 0.1.1-rc.2 baseline exposes the log as
|
|
59
|
+
* `events` (+ `header.seedLength`), while 0.1.2-alpha.4+ exposes
|
|
60
|
+
* `snapshotEvents()` and `inheritedEventCount` (and dropped the header field).
|
|
61
|
+
*/
|
|
49
62
|
export interface ScannerSession {
|
|
50
63
|
readonly id: SessionId;
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
/** ≤ 0.1.1-rc.2: the full event log snapshot. */
|
|
65
|
+
readonly events?: readonly SessionEvent[];
|
|
66
|
+
/** 0.1.2-alpha.4+: materialize an immutable log snapshot; no args = the full current log. */
|
|
67
|
+
readonly snapshotEvents?: (fromSeq?: number, toSeqExclusive?: number) => readonly SessionEvent[];
|
|
68
|
+
/** 0.1.2-alpha.4+: the durable inherited-prefix length (0 for an unseeded session). */
|
|
69
|
+
readonly inheritedEventCount?: number;
|
|
70
|
+
/** Durable header slice: `seedLength` (older runtime) or `isSeeded` (newer runtime). */
|
|
53
71
|
readonly header?: {
|
|
54
72
|
readonly seedLength?: number;
|
|
73
|
+
readonly isSeeded?: boolean;
|
|
55
74
|
};
|
|
56
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Read one live session's complete event log across both runtime families.
|
|
78
|
+
* @throws when the session exposes neither the legacy `events` snapshot nor
|
|
79
|
+
* the newer `snapshotEvents()` reader — an unknown runtime surface must
|
|
80
|
+
* fail loudly rather than silently price an empty log.
|
|
81
|
+
*/
|
|
82
|
+
export declare function liveSessionEvents(session: ScannerSession): readonly SessionEvent[];
|
|
57
83
|
/** Structural slice of a listed persisted session (the snapshot header is a full SessionHeader). */
|
|
58
84
|
export interface ScannerPersistedHeader {
|
|
59
85
|
readonly id: SessionId;
|
|
60
|
-
/**
|
|
86
|
+
/** ≤ 0.1.1-rc.2: the durable fork boundary carried by the snapshot header; absent for an unseeded session. */
|
|
61
87
|
readonly seedLength?: number;
|
|
88
|
+
/** 0.1.2-alpha.4+: whether the session has a fork-inherited prefix (exact cut arrives with the inspect result). */
|
|
89
|
+
readonly isSeeded?: boolean;
|
|
62
90
|
}
|
|
63
91
|
/** Structural slices of the optional services the scanner reads through. */
|
|
64
92
|
export interface TodaySpendScannerDeps {
|
|
@@ -73,9 +101,12 @@ export interface TodaySpendScannerDeps {
|
|
|
73
101
|
revision: SessionPersistenceRevision;
|
|
74
102
|
}[]>;
|
|
75
103
|
inspect(id: SessionId): Promise<{
|
|
76
|
-
meta
|
|
77
|
-
seedLength?: number;
|
|
104
|
+
meta?: {
|
|
105
|
+
readonly seedLength?: number;
|
|
106
|
+
readonly isSeeded?: boolean;
|
|
78
107
|
};
|
|
108
|
+
/** 0.1.2-alpha.4+: the exact inherited cut travels beside, not inside, the header. */
|
|
109
|
+
inheritedEventCount?: number;
|
|
79
110
|
events: readonly SessionEvent[];
|
|
80
111
|
}>;
|
|
81
112
|
} | undefined;
|
|
@@ -179,9 +210,12 @@ export declare class TodaySpendScanner {
|
|
|
179
210
|
* until the session is inspected again. A SEEDED session (fork child)
|
|
180
211
|
* skips the ladder entirely: its cached row was folded over the inherited
|
|
181
212
|
* prefix too, so it always detaches through inspect with the durable
|
|
182
|
-
* boundary
|
|
213
|
+
* boundary (the inspect result's inherited count or `meta.seedLength`,
|
|
214
|
+
* depending on the runtime family) applied to the local fold.
|
|
183
215
|
* @param id - the cold session's id.
|
|
184
|
-
* @param
|
|
216
|
+
* @param seeded - whether the session carries a fork-inherited prefix
|
|
217
|
+
* (from the snapshot header: `isSeeded` on 0.1.2-alpha.4+, `seedLength`
|
|
218
|
+
* at and before the 0.1.1-rc.2 baseline).
|
|
185
219
|
* @returns the resolved state and title, or `undefined` when unreadable.
|
|
186
220
|
*/
|
|
187
221
|
private resolveCold;
|
package/lib/types/today-spend.js
CHANGED
|
@@ -20,17 +20,25 @@
|
|
|
20
20
|
* unchanged log provably cannot change the aggregate.
|
|
21
21
|
*
|
|
22
22
|
* Forked sessions never double-count: a fork child's log opens with a
|
|
23
|
-
* verbatim copy of its source session's events (
|
|
24
|
-
* so the scanner prices only the child's OWN events
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* its original boundary and an unseeded session stays at 0.
|
|
23
|
+
* verbatim copy of its source session's events (its inherited boundary),
|
|
24
|
+
* so the scanner prices only the child's OWN events on every path — the
|
|
25
|
+
* projection path bypasses the eager cell for a seeded session and folds its
|
|
26
|
+
* own events instead (the cell covers the inherited prefix too), and the cold
|
|
27
|
+
* ladder skips the projection cache for a seeded session (its cached row
|
|
28
|
+
* predates the boundary and covers inherited events). The boundary is the
|
|
29
|
+
* durable session state, read across both DSH runtime families — a resumed
|
|
30
|
+
* fork child keeps its original boundary and an unseeded session stays at 0.
|
|
31
|
+
*
|
|
32
|
+
* The live `Session` log surface changed in 0.1.2-alpha.4: `Session.events`
|
|
33
|
+
* was removed and replaced by `Session.snapshotEvents()` / `ownEvents()`, and
|
|
34
|
+
* `SessionHeader.seedLength` moved to `Session.inheritedEventCount` (the
|
|
35
|
+
* persistence `inspect` result carries it alongside `meta`). Reads go through
|
|
36
|
+
* {@link liveSessionEvents} / {@link forkBoundaryOf}, which accept both
|
|
37
|
+
* families structurally, so the scanner runs on the ≤ 0.1.1-rc.2 npm baseline
|
|
38
|
+
* and on the newer runtime.
|
|
31
39
|
* @module @rayadesu/dsh-llm-billing/today-spend
|
|
32
40
|
*/
|
|
33
|
-
import { beijingDayKey, emptyTodaySpend, forkBoundaryOf, mergeTodaySpend, priceEvent, SpendAccumulator } from "./billing.js";
|
|
41
|
+
import { beijingDayKey, emptyTodaySpend, forkBoundaryOf, isSeededSession, mergeTodaySpend, priceEvent, SpendAccumulator } from "./billing.js";
|
|
34
42
|
import { BILLING_UNIT_KEY, foldOwnBilling } from "./projection.js";
|
|
35
43
|
/**
|
|
36
44
|
* Fold one session's durable display title: the latest `session/title`
|
|
@@ -53,6 +61,20 @@ export function foldSessionTitle(events) {
|
|
|
53
61
|
}
|
|
54
62
|
return null;
|
|
55
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Read one live session's complete event log across both runtime families.
|
|
66
|
+
* @throws when the session exposes neither the legacy `events` snapshot nor
|
|
67
|
+
* the newer `snapshotEvents()` reader — an unknown runtime surface must
|
|
68
|
+
* fail loudly rather than silently price an empty log.
|
|
69
|
+
*/
|
|
70
|
+
export function liveSessionEvents(session) {
|
|
71
|
+
if (session.events !== undefined)
|
|
72
|
+
return session.events;
|
|
73
|
+
// Optional-call form keeps the receiver bound (snapshotEvents uses `this`).
|
|
74
|
+
if (session.snapshotEvents !== undefined)
|
|
75
|
+
return session.snapshotEvents();
|
|
76
|
+
throw new Error('llm-billing: session log surface is neither Session.events nor Session.snapshotEvents');
|
|
77
|
+
}
|
|
56
78
|
/**
|
|
57
79
|
* Bounded parallel fan-out: run `run` over `items` with at most `limit` in
|
|
58
80
|
* flight. A shared index counter hands each worker its next job, so the
|
|
@@ -176,17 +198,20 @@ export class TodaySpendScanner {
|
|
|
176
198
|
* until the session is inspected again. A SEEDED session (fork child)
|
|
177
199
|
* skips the ladder entirely: its cached row was folded over the inherited
|
|
178
200
|
* prefix too, so it always detaches through inspect with the durable
|
|
179
|
-
* boundary
|
|
201
|
+
* boundary (the inspect result's inherited count or `meta.seedLength`,
|
|
202
|
+
* depending on the runtime family) applied to the local fold.
|
|
180
203
|
* @param id - the cold session's id.
|
|
181
|
-
* @param
|
|
204
|
+
* @param seeded - whether the session carries a fork-inherited prefix
|
|
205
|
+
* (from the snapshot header: `isSeeded` on 0.1.2-alpha.4+, `seedLength`
|
|
206
|
+
* at and before the 0.1.1-rc.2 baseline).
|
|
182
207
|
* @returns the resolved state and title, or `undefined` when unreadable.
|
|
183
208
|
*/
|
|
184
|
-
async resolveCold(id,
|
|
185
|
-
const { persistence, projectionCache,
|
|
209
|
+
async resolveCold(id, seeded) {
|
|
210
|
+
const { persistence, projectionCache, logger } = this.deps;
|
|
186
211
|
const persistenceService = persistence?.();
|
|
187
212
|
if (persistenceService === undefined)
|
|
188
213
|
return undefined;
|
|
189
|
-
if (
|
|
214
|
+
if (!seeded) {
|
|
190
215
|
const cache = projectionCache?.();
|
|
191
216
|
if (cache !== undefined) {
|
|
192
217
|
try {
|
|
@@ -201,7 +226,10 @@ export class TodaySpendScanner {
|
|
|
201
226
|
}
|
|
202
227
|
try {
|
|
203
228
|
const inspection = await persistenceService.inspect(id);
|
|
204
|
-
return {
|
|
229
|
+
return {
|
|
230
|
+
value: foldOwnBilling(this.deps.unit, inspection.events, forkBoundaryOf(inspection)),
|
|
231
|
+
title: foldSessionTitle(inspection.events),
|
|
232
|
+
};
|
|
205
233
|
}
|
|
206
234
|
catch (error) {
|
|
207
235
|
// One unreadable session must not blank the whole-day aggregate.
|
|
@@ -250,11 +278,11 @@ export class TodaySpendScanner {
|
|
|
250
278
|
if (store !== undefined) {
|
|
251
279
|
for (const session of store.list()) {
|
|
252
280
|
liveIds.add(session.id);
|
|
253
|
-
const seedLength = forkBoundaryOf(session
|
|
281
|
+
const seedLength = forkBoundaryOf(session);
|
|
254
282
|
// A fork child's eager cell covers its inherited prefix too; price
|
|
255
283
|
// its own events directly instead.
|
|
256
284
|
const state = seedLength > 0
|
|
257
|
-
? this.ownBillingState(session.id, session
|
|
285
|
+
? this.ownBillingState(session.id, liveSessionEvents(session), seedLength)
|
|
258
286
|
: projectionsService?.stateOf(session, BILLING_UNIT_KEY);
|
|
259
287
|
if (state !== undefined && state.dayKey === dayKey) {
|
|
260
288
|
total = mergeTodaySpend(total, state.spend);
|
|
@@ -270,17 +298,17 @@ export class TodaySpendScanner {
|
|
|
270
298
|
for (const { header, revision } of snapshots) {
|
|
271
299
|
if (liveIds.has(header.id))
|
|
272
300
|
continue;
|
|
273
|
-
const
|
|
301
|
+
const seeded = isSeededSession(header);
|
|
274
302
|
const resolved = this.coldResolved.get(header.id);
|
|
275
303
|
if (resolved !== undefined && resolved.revision === revision) {
|
|
276
304
|
if (resolved.value.dayKey === dayKey)
|
|
277
305
|
total = mergeTodaySpend(total, resolved.value.spend);
|
|
278
306
|
continue;
|
|
279
307
|
}
|
|
280
|
-
pending.push({ id: header.id, revision,
|
|
308
|
+
pending.push({ id: header.id, revision, seeded });
|
|
281
309
|
}
|
|
282
|
-
await withConcurrency(pending, 8, async ({ id, revision,
|
|
283
|
-
const resolved = await this.resolveCold(id,
|
|
310
|
+
await withConcurrency(pending, 8, async ({ id, revision, seeded }) => {
|
|
311
|
+
const resolved = await this.resolveCold(id, seeded);
|
|
284
312
|
if (resolved !== undefined)
|
|
285
313
|
this.coldResolved.set(id, { revision, ...resolved });
|
|
286
314
|
});
|
|
@@ -326,7 +354,7 @@ export class TodaySpendScanner {
|
|
|
326
354
|
if (store !== undefined) {
|
|
327
355
|
for (const session of store.list()) {
|
|
328
356
|
liveIds.add(session.id);
|
|
329
|
-
collect(session
|
|
357
|
+
collect(liveSessionEvents(session), forkBoundaryOf(session));
|
|
330
358
|
if (truncated)
|
|
331
359
|
break;
|
|
332
360
|
}
|
|
@@ -342,7 +370,7 @@ export class TodaySpendScanner {
|
|
|
342
370
|
continue;
|
|
343
371
|
try {
|
|
344
372
|
const inspection = await persistenceService.inspect(header.id);
|
|
345
|
-
collect(inspection.events, forkBoundaryOf(inspection
|
|
373
|
+
collect(inspection.events, forkBoundaryOf(inspection));
|
|
346
374
|
}
|
|
347
375
|
catch (error) {
|
|
348
376
|
// One unreadable session must not blank the whole-day aggregate.
|
|
@@ -381,14 +409,15 @@ export class TodaySpendScanner {
|
|
|
381
409
|
if (store !== undefined) {
|
|
382
410
|
for (const session of store.list()) {
|
|
383
411
|
liveIds.add(session.id);
|
|
384
|
-
const seedLength = forkBoundaryOf(session
|
|
412
|
+
const seedLength = forkBoundaryOf(session);
|
|
413
|
+
const events = liveSessionEvents(session);
|
|
385
414
|
const state = seedLength > 0
|
|
386
|
-
? this.ownBillingState(session.id,
|
|
415
|
+
? this.ownBillingState(session.id, events, seedLength)
|
|
387
416
|
: projectionsService?.stateOf(session, BILLING_UNIT_KEY);
|
|
388
417
|
if (state !== undefined && state.dayKey === dayKey) {
|
|
389
418
|
rows.set(session.id, {
|
|
390
419
|
sessionId: session.id,
|
|
391
|
-
title: foldSessionTitle(
|
|
420
|
+
title: foldSessionTitle(events),
|
|
392
421
|
total: state.spend.total,
|
|
393
422
|
});
|
|
394
423
|
}
|
|
@@ -403,7 +432,7 @@ export class TodaySpendScanner {
|
|
|
403
432
|
for (const { header, revision } of snapshots) {
|
|
404
433
|
if (liveIds.has(header.id))
|
|
405
434
|
continue;
|
|
406
|
-
const
|
|
435
|
+
const seeded = isSeededSession(header);
|
|
407
436
|
const resolved = this.coldResolved.get(header.id);
|
|
408
437
|
if (resolved !== undefined && resolved.revision === revision) {
|
|
409
438
|
if (resolved.value.dayKey === dayKey) {
|
|
@@ -411,10 +440,10 @@ export class TodaySpendScanner {
|
|
|
411
440
|
}
|
|
412
441
|
continue;
|
|
413
442
|
}
|
|
414
|
-
pending.push({ id: header.id, revision,
|
|
443
|
+
pending.push({ id: header.id, revision, seeded });
|
|
415
444
|
}
|
|
416
|
-
await withConcurrency(pending, 8, async ({ id, revision,
|
|
417
|
-
const resolved = await this.resolveCold(id,
|
|
445
|
+
await withConcurrency(pending, 8, async ({ id, revision, seeded }) => {
|
|
446
|
+
const resolved = await this.resolveCold(id, seeded);
|
|
418
447
|
if (resolved !== undefined)
|
|
419
448
|
this.coldResolved.set(id, { revision, ...resolved });
|
|
420
449
|
});
|
|
@@ -470,7 +499,7 @@ export class TodaySpendScanner {
|
|
|
470
499
|
if (store !== undefined) {
|
|
471
500
|
for (const session of store.list()) {
|
|
472
501
|
liveIds.add(session.id);
|
|
473
|
-
collect(session.id, session
|
|
502
|
+
collect(session.id, liveSessionEvents(session), forkBoundaryOf(session));
|
|
474
503
|
if (truncated)
|
|
475
504
|
break;
|
|
476
505
|
}
|
|
@@ -486,7 +515,7 @@ export class TodaySpendScanner {
|
|
|
486
515
|
continue;
|
|
487
516
|
try {
|
|
488
517
|
const inspection = await persistenceService.inspect(header.id);
|
|
489
|
-
collect(header.id, inspection.events, forkBoundaryOf(inspection
|
|
518
|
+
collect(header.id, inspection.events, forkBoundaryOf(inspection));
|
|
490
519
|
}
|
|
491
520
|
catch (error) {
|
|
492
521
|
// One unreadable session must not blank the whole-day aggregate.
|
package/package.json
CHANGED