@rayadesu/dsh-llm-billing 0.3.5 → 0.3.7
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 +7 -1
- package/README.zh.md +7 -1
- package/lib/index.js +129 -60
- package/lib/types/billing.d.ts +45 -12
- package/lib/types/billing.js +24 -12
- package/lib/types/index.d.ts +4 -4
- package/lib/types/index.js +11 -8
- package/lib/types/projection.d.ts +14 -2
- package/lib/types/today-spend.d.ts +109 -29
- package/lib/types/today-spend.js +104 -39
- package/package.json +17 -17
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: 3c0ae19286d6935190659ec1e1c93b7b63ab5c14
|
|
6
|
+
README.zh.md: 08adb0d018b376fb5fa995b29cbb6790063f4814
|
package/README.md
CHANGED
|
@@ -31,7 +31,13 @@ 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.2-alpha.5`) 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.
|
|
39
|
+
|
|
40
|
+
The persistence service itself changed surface too: 0.1.1-rc.2 exposes `inspect(id)` / `listSnapshots()`, while the 0.1.2-alpha.5 handle-based seam (in the monorepo after the released build) exposes `open(id, 'read')` + `SessionHandle.read()` / `list()`. The scanner reads both families through `persistenceInspect` / `persistenceListSnapshots` (the handle is always closed, including after a failed read), so the same plugin serves the published alpha line and the refactored checkout.
|
|
35
41
|
|
|
36
42
|
## Configuration
|
|
37
43
|
|
package/README.zh.md
CHANGED
|
@@ -31,7 +31,13 @@
|
|
|
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.2-alpha.5`)与超前于它的 monorepo 运行时代码均无需改动即可工作。遇到两种形状都没有的未知运行时表面时,插件会显式失败而不是静默按零花费计价。
|
|
39
|
+
|
|
40
|
+
持久化服务的表面同样换代:0.1.1-rc.2 提供 `inspect(id)` / `listSnapshots()`,而 handle 化的 0.1.2-alpha.5 seam(位于已发布构建之后的 monorepo 中)提供 `open(id, 'read')` + `SessionHandle.read()` / `list()`。扫描器通过 `persistenceInspect` / `persistenceListSnapshots` 同时读取两代表面(handle 总会关闭,读取失败时也一样),因此同一套插件既能服务已发布的 alpha 线,也能服务重构后的 checkout。
|
|
35
41
|
|
|
36
42
|
## 配置
|
|
37
43
|
|
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,56 @@ 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
|
+
function isHandlePersistence(persistence) {
|
|
840
|
+
return typeof persistence.open === "function";
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* List every stored session snapshot across both persistence runtime families:
|
|
844
|
+
* `listSnapshots` (≤ 0.1.1-rc.2) or `list` (0.1.2-alpha.5+).
|
|
845
|
+
* @param persistence - the persistence service slice.
|
|
846
|
+
* @returns one snapshot per stored session.
|
|
847
|
+
*/
|
|
848
|
+
function persistenceListSnapshots(persistence) {
|
|
849
|
+
return isHandlePersistence(persistence) ? persistence.list() : persistence.listSnapshots();
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Read one stored session's complete event log and durable inherited boundary
|
|
853
|
+
* across both persistence runtime families: legacy `inspect` (≤ 0.1.1-rc.2)
|
|
854
|
+
* or `open` + handle `read` (0.1.2-alpha.5+; the handle is closed after the
|
|
855
|
+
* read). Both throw when the session does not exist.
|
|
856
|
+
* @param persistence - the persistence service slice.
|
|
857
|
+
* @param id - the stored session to read.
|
|
858
|
+
* @returns the session's complete event log plus its inherited-prefix boundary.
|
|
859
|
+
*/
|
|
860
|
+
async function persistenceInspect(persistence, id) {
|
|
861
|
+
if (isHandlePersistence(persistence)) {
|
|
862
|
+
const handle = await persistence.open(id, "read");
|
|
863
|
+
try {
|
|
864
|
+
return {
|
|
865
|
+
events: await handle.read(),
|
|
866
|
+
seedLength: forkBoundaryOf(handle)
|
|
867
|
+
};
|
|
868
|
+
} finally {
|
|
869
|
+
await handle.close();
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const inspection = await persistence.inspect(id);
|
|
873
|
+
return {
|
|
874
|
+
events: inspection.events,
|
|
875
|
+
seedLength: forkBoundaryOf(inspection)
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
813
879
|
* Bounded parallel fan-out: run `run` over `items` with at most `limit` in
|
|
814
880
|
* flight. A shared index counter hands each worker its next job, so the
|
|
815
881
|
* dispatch is O(n) overall (array `shift()` would be O(n) per pop).
|
|
@@ -923,16 +989,19 @@ var TodaySpendScanner = class {
|
|
|
923
989
|
* until the session is inspected again. A SEEDED session (fork child)
|
|
924
990
|
* skips the ladder entirely: its cached row was folded over the inherited
|
|
925
991
|
* prefix too, so it always detaches through inspect with the durable
|
|
926
|
-
* boundary
|
|
992
|
+
* boundary (the inspect result's inherited count or `meta.seedLength`,
|
|
993
|
+
* depending on the runtime family) applied to the local fold.
|
|
927
994
|
* @param id - the cold session's id.
|
|
928
|
-
* @param
|
|
995
|
+
* @param seeded - whether the session carries a fork-inherited prefix
|
|
996
|
+
* (from the snapshot header: `isSeeded` on 0.1.2-alpha.4+, `seedLength`
|
|
997
|
+
* at and before the 0.1.1-rc.2 baseline).
|
|
929
998
|
* @returns the resolved state and title, or `undefined` when unreadable.
|
|
930
999
|
*/
|
|
931
|
-
async resolveCold(id,
|
|
932
|
-
const { persistence, projectionCache,
|
|
1000
|
+
async resolveCold(id, seeded) {
|
|
1001
|
+
const { persistence, projectionCache, logger } = this.deps;
|
|
933
1002
|
const persistenceService = persistence?.();
|
|
934
1003
|
if (persistenceService === void 0) return void 0;
|
|
935
|
-
if (
|
|
1004
|
+
if (!seeded) {
|
|
936
1005
|
const cache = projectionCache?.();
|
|
937
1006
|
if (cache !== void 0) try {
|
|
938
1007
|
const value = (await cache.coldSnapshot(id)).values[BILLING_UNIT_KEY];
|
|
@@ -945,10 +1014,10 @@ var TodaySpendScanner = class {
|
|
|
945
1014
|
}
|
|
946
1015
|
}
|
|
947
1016
|
try {
|
|
948
|
-
const
|
|
1017
|
+
const read = await persistenceInspect(persistenceService, id);
|
|
949
1018
|
return {
|
|
950
|
-
value: foldOwnBilling(unit,
|
|
951
|
-
title: foldSessionTitle(
|
|
1019
|
+
value: foldOwnBilling(this.deps.unit, read.events, read.seedLength),
|
|
1020
|
+
title: foldSessionTitle(read.events)
|
|
952
1021
|
};
|
|
953
1022
|
} catch (error) {
|
|
954
1023
|
logger.warn(`llm-billing: skipping unreadable session ${id}: ${String(error)}`);
|
|
@@ -992,18 +1061,18 @@ var TodaySpendScanner = class {
|
|
|
992
1061
|
const store = sessions();
|
|
993
1062
|
if (store !== void 0) for (const session of store.list()) {
|
|
994
1063
|
liveIds.add(session.id);
|
|
995
|
-
const seedLength = forkBoundaryOf(session
|
|
996
|
-
const state = seedLength > 0 ? this.ownBillingState(session.id, session
|
|
1064
|
+
const seedLength = forkBoundaryOf(session);
|
|
1065
|
+
const state = seedLength > 0 ? this.ownBillingState(session.id, liveSessionEvents(session), seedLength) : projectionsService?.stateOf(session, BILLING_UNIT_KEY);
|
|
997
1066
|
if (state !== void 0 && state.dayKey === dayKey) total = mergeTodaySpend(total, state.spend);
|
|
998
1067
|
}
|
|
999
1068
|
}
|
|
1000
1069
|
const persistenceService = persistence?.();
|
|
1001
1070
|
if (persistenceService === void 0) return total;
|
|
1002
|
-
const snapshots = await persistenceService
|
|
1071
|
+
const snapshots = await persistenceListSnapshots(persistenceService);
|
|
1003
1072
|
const pending = [];
|
|
1004
1073
|
for (const { header, revision } of snapshots) {
|
|
1005
1074
|
if (liveIds.has(header.id)) continue;
|
|
1006
|
-
const
|
|
1075
|
+
const seeded = isSeededSession(header);
|
|
1007
1076
|
const resolved = this.coldResolved.get(header.id);
|
|
1008
1077
|
if (resolved !== void 0 && resolved.revision === revision) {
|
|
1009
1078
|
if (resolved.value.dayKey === dayKey) total = mergeTodaySpend(total, resolved.value.spend);
|
|
@@ -1012,11 +1081,11 @@ var TodaySpendScanner = class {
|
|
|
1012
1081
|
pending.push({
|
|
1013
1082
|
id: header.id,
|
|
1014
1083
|
revision,
|
|
1015
|
-
|
|
1084
|
+
seeded
|
|
1016
1085
|
});
|
|
1017
1086
|
}
|
|
1018
|
-
await withConcurrency(pending, 8, async ({ id, revision,
|
|
1019
|
-
const resolved = await this.resolveCold(id,
|
|
1087
|
+
await withConcurrency(pending, 8, async ({ id, revision, seeded }) => {
|
|
1088
|
+
const resolved = await this.resolveCold(id, seeded);
|
|
1020
1089
|
if (resolved !== void 0) this.coldResolved.set(id, {
|
|
1021
1090
|
revision,
|
|
1022
1091
|
...resolved
|
|
@@ -1058,19 +1127,19 @@ var TodaySpendScanner = class {
|
|
|
1058
1127
|
const store = sessions();
|
|
1059
1128
|
if (store !== void 0) for (const session of store.list()) {
|
|
1060
1129
|
liveIds.add(session.id);
|
|
1061
|
-
collect(session
|
|
1130
|
+
collect(liveSessionEvents(session), forkBoundaryOf(session));
|
|
1062
1131
|
if (truncated) break;
|
|
1063
1132
|
}
|
|
1064
1133
|
}
|
|
1065
1134
|
const persistenceService = persistence?.();
|
|
1066
1135
|
if (!truncated && persistenceService !== void 0) {
|
|
1067
|
-
const snapshots = await persistenceService
|
|
1136
|
+
const snapshots = await persistenceListSnapshots(persistenceService);
|
|
1068
1137
|
for (const { header, revision } of snapshots) {
|
|
1069
1138
|
if (liveIds.has(header.id)) continue;
|
|
1070
1139
|
if (this.lastEventsScan?.get(header.id) === revision) continue;
|
|
1071
1140
|
try {
|
|
1072
|
-
const
|
|
1073
|
-
collect(
|
|
1141
|
+
const read = await persistenceInspect(persistenceService, header.id);
|
|
1142
|
+
collect(read.events, read.seedLength);
|
|
1074
1143
|
} catch (error) {
|
|
1075
1144
|
logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
|
|
1076
1145
|
}
|
|
@@ -1099,22 +1168,23 @@ var TodaySpendScanner = class {
|
|
|
1099
1168
|
const store = sessions();
|
|
1100
1169
|
if (store !== void 0) for (const session of store.list()) {
|
|
1101
1170
|
liveIds.add(session.id);
|
|
1102
|
-
const seedLength = forkBoundaryOf(session
|
|
1103
|
-
const
|
|
1171
|
+
const seedLength = forkBoundaryOf(session);
|
|
1172
|
+
const events = liveSessionEvents(session);
|
|
1173
|
+
const state = seedLength > 0 ? this.ownBillingState(session.id, events, seedLength) : projectionsService?.stateOf(session, BILLING_UNIT_KEY);
|
|
1104
1174
|
if (state !== void 0 && state.dayKey === dayKey) rows.set(session.id, {
|
|
1105
1175
|
sessionId: session.id,
|
|
1106
|
-
title: foldSessionTitle(
|
|
1176
|
+
title: foldSessionTitle(events),
|
|
1107
1177
|
total: state.spend.total
|
|
1108
1178
|
});
|
|
1109
1179
|
}
|
|
1110
1180
|
}
|
|
1111
1181
|
const persistenceService = persistence?.();
|
|
1112
1182
|
if (persistenceService === void 0) return [...rows.values()];
|
|
1113
|
-
const snapshots = await persistenceService
|
|
1183
|
+
const snapshots = await persistenceListSnapshots(persistenceService);
|
|
1114
1184
|
const pending = [];
|
|
1115
1185
|
for (const { header, revision } of snapshots) {
|
|
1116
1186
|
if (liveIds.has(header.id)) continue;
|
|
1117
|
-
const
|
|
1187
|
+
const seeded = isSeededSession(header);
|
|
1118
1188
|
const resolved = this.coldResolved.get(header.id);
|
|
1119
1189
|
if (resolved !== void 0 && resolved.revision === revision) {
|
|
1120
1190
|
if (resolved.value.dayKey === dayKey) rows.set(header.id, {
|
|
@@ -1127,11 +1197,11 @@ var TodaySpendScanner = class {
|
|
|
1127
1197
|
pending.push({
|
|
1128
1198
|
id: header.id,
|
|
1129
1199
|
revision,
|
|
1130
|
-
|
|
1200
|
+
seeded
|
|
1131
1201
|
});
|
|
1132
1202
|
}
|
|
1133
|
-
await withConcurrency(pending, 8, async ({ id, revision,
|
|
1134
|
-
const resolved = await this.resolveCold(id,
|
|
1203
|
+
await withConcurrency(pending, 8, async ({ id, revision, seeded }) => {
|
|
1204
|
+
const resolved = await this.resolveCold(id, seeded);
|
|
1135
1205
|
if (resolved !== void 0) this.coldResolved.set(id, {
|
|
1136
1206
|
revision,
|
|
1137
1207
|
...resolved
|
|
@@ -1190,19 +1260,19 @@ var TodaySpendScanner = class {
|
|
|
1190
1260
|
const store = sessions();
|
|
1191
1261
|
if (store !== void 0) for (const session of store.list()) {
|
|
1192
1262
|
liveIds.add(session.id);
|
|
1193
|
-
collect(session.id, session
|
|
1263
|
+
collect(session.id, liveSessionEvents(session), forkBoundaryOf(session));
|
|
1194
1264
|
if (truncated) break;
|
|
1195
1265
|
}
|
|
1196
1266
|
}
|
|
1197
1267
|
const persistenceService = persistence?.();
|
|
1198
1268
|
if (!truncated && persistenceService !== void 0) {
|
|
1199
|
-
const snapshots = await persistenceService
|
|
1269
|
+
const snapshots = await persistenceListSnapshots(persistenceService);
|
|
1200
1270
|
for (const { header, revision } of snapshots) {
|
|
1201
1271
|
if (liveIds.has(header.id)) continue;
|
|
1202
1272
|
if (this.lastEventsScan?.get(header.id) === revision) continue;
|
|
1203
1273
|
try {
|
|
1204
|
-
const
|
|
1205
|
-
collect(header.id,
|
|
1274
|
+
const read = await persistenceInspect(persistenceService, header.id);
|
|
1275
|
+
collect(header.id, read.events, read.seedLength);
|
|
1206
1276
|
} catch (error) {
|
|
1207
1277
|
logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
|
|
1208
1278
|
}
|
|
@@ -1294,7 +1364,10 @@ const TODAY_SPEND_MAX_EVENTS = 2e5;
|
|
|
1294
1364
|
/**
|
|
1295
1365
|
* Read one session's event log and durable seed boundary: the live
|
|
1296
1366
|
* SessionStore first, then the persistence backend for a flushed session
|
|
1297
|
-
* (inspected directly by id — no header listing).
|
|
1367
|
+
* (inspected directly by id — no header listing). The live-session surface
|
|
1368
|
+
* is read structurally across both DSH runtime families — `Session.events`
|
|
1369
|
+
* (≤ 0.1.1-rc.2) or `Session.snapshotEvents()` + `Session.inheritedEventCount`
|
|
1370
|
+
* (0.1.2-alpha.4+) — via {@link liveSessionEvents} / {@link forkBoundaryOf}.
|
|
1298
1371
|
* @param ctx - plugin context carrying the SessionStore and optional persistence.
|
|
1299
1372
|
* @param sessionId - the session to read.
|
|
1300
1373
|
* @returns the session's complete event log plus its inherited-prefix boundary.
|
|
@@ -1303,16 +1376,12 @@ const TODAY_SPEND_MAX_EVENTS = 2e5;
|
|
|
1303
1376
|
async function sessionEvents(ctx, sessionId) {
|
|
1304
1377
|
const live = ctx.get("sessions")?.get(sessionId);
|
|
1305
1378
|
if (live !== void 0) return {
|
|
1306
|
-
events: live
|
|
1307
|
-
seedLength: forkBoundaryOf(live
|
|
1379
|
+
events: liveSessionEvents(live),
|
|
1380
|
+
seedLength: forkBoundaryOf(live)
|
|
1308
1381
|
};
|
|
1309
1382
|
const persistence = ctx.get("sessionPersistence");
|
|
1310
1383
|
if (persistence !== void 0) try {
|
|
1311
|
-
|
|
1312
|
-
return {
|
|
1313
|
-
events: inspection.events,
|
|
1314
|
-
seedLength: forkBoundaryOf(inspection.meta)
|
|
1315
|
-
};
|
|
1384
|
+
return await persistenceInspect(persistence, sessionId);
|
|
1316
1385
|
} catch (error) {
|
|
1317
1386
|
throw new LlmError(`llm-billing: session ${sessionId} not found`, "NOT_FOUND", { cause: error });
|
|
1318
1387
|
}
|
|
@@ -1406,4 +1475,4 @@ function apply(ctx, config) {
|
|
|
1406
1475
|
});
|
|
1407
1476
|
}
|
|
1408
1477
|
//#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 };
|
|
1478
|
+
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, persistenceInspect, persistenceListSnapshots, priceEvent, resolveBilling };
|
package/lib/types/billing.d.ts
CHANGED
|
@@ -88,21 +88,54 @@ 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
|
+
readonly isSeeded?: boolean;
|
|
114
|
+
};
|
|
115
|
+
/** ≤ 0.1.1-rc.2: the `meta` of a persistence `inspect` result (its `seedLength`). */
|
|
116
|
+
readonly meta?: {
|
|
117
|
+
readonly seedLength?: number;
|
|
118
|
+
readonly isSeeded?: boolean;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The durable inherited-prefix boundary of one session: the number of leading
|
|
123
|
+
* events it inherited verbatim from its fork source, 0 for a session created
|
|
124
|
+
* without a seed. A forked session (or any seeded replay) carries that count
|
|
125
|
+
* in its session state; every event with `seq < seedLength` is a copy of an
|
|
126
|
+
* event already billed in that source session, so pricing must skip them or
|
|
127
|
+
* the same model output is counted once per copy. Accepts the durable field
|
|
128
|
+
* of both DSH runtime families (see {@link ForkBoundarySource}).
|
|
129
|
+
* @param source - the session, inspection result, header slice, or durable
|
|
130
|
+
* header carrying the boundary; `undefined` reads as 0.
|
|
101
131
|
* @returns the inherited-prefix length; 0 for an unseeded session.
|
|
102
132
|
*/
|
|
103
|
-
export declare function forkBoundaryOf(
|
|
133
|
+
export declare function forkBoundaryOf(source: ForkBoundarySource | undefined): number;
|
|
134
|
+
/** Whether a durable header marks a fork-inherited (seeded) session across both runtime families. */
|
|
135
|
+
export declare function isSeededSession(header: {
|
|
104
136
|
readonly seedLength?: number;
|
|
105
|
-
|
|
137
|
+
readonly isSeeded?: boolean;
|
|
138
|
+
} | undefined): boolean;
|
|
106
139
|
/**
|
|
107
140
|
* Whether a timestamp falls inside any peak-hour window (Beijing time,
|
|
108
141
|
* 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,13 +24,13 @@ 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
|
-
export type { BillingUnitState } from './projection.ts';
|
|
32
|
-
export { foldSessionTitle, TodaySpendCache, TodaySpendScanner } from './today-spend.ts';
|
|
33
|
-
export type { ScannerPersistedHeader, ScannerSession, TodaySpendScannerDeps } from './today-spend.ts';
|
|
31
|
+
export type { BillingUnitFold, BillingUnitState } from './projection.ts';
|
|
32
|
+
export { foldSessionTitle, liveSessionEvents, persistenceInspect, persistenceListSnapshots, TodaySpendCache, TodaySpendScanner } from './today-spend.ts';
|
|
33
|
+
export type { ScannerPersistedHeader, ScannerPersistence, ScannerPersistenceHandle, ScannerPersistenceLegacy, ScannerPersistedRead, 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. */
|
|
36
36
|
export declare const PUBLIC_BASE_URL = "https://api.deepseek.com";
|
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, persistenceInspect, 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, persistenceInspect, persistenceListSnapshots, 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,13 @@ 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
|
-
|
|
93
|
-
return { events: inspection.events, seedLength: forkBoundaryOf(inspection.meta) };
|
|
96
|
+
return await persistenceInspect(persistence, sessionId);
|
|
94
97
|
}
|
|
95
98
|
catch (error) {
|
|
96
99
|
throw new LlmError(`llm-billing: session ${sessionId} not found`, 'NOT_FOUND', { cause: error });
|
|
@@ -64,8 +64,20 @@ export declare function billingTodaySpendDefinition(billing: ResolvedBilling, ca
|
|
|
64
64
|
id: string;
|
|
65
65
|
name: string;
|
|
66
66
|
}[]): BillingUnitDefinition;
|
|
67
|
+
/** The fold halves of a billing unit, as the detached cold recipes call them. */
|
|
68
|
+
export interface BillingUnitFold {
|
|
69
|
+
/**
|
|
70
|
+
* Initial state for the empty log. DSH ≤ 0.1.1-rc.2 declared `init()` with
|
|
71
|
+
* no parameters; 0.1.2-alpha.5+ passes the Session header and inherited
|
|
72
|
+
* count. The unit ignores both, so the structural type accepts either call
|
|
73
|
+
* shape.
|
|
74
|
+
*/
|
|
75
|
+
init(...metadata: never[]): BillingUnitState;
|
|
76
|
+
/** Pure transition: previous state + one committed event → next state. */
|
|
77
|
+
apply(state: BillingUnitState, event: SessionEvent): BillingUnitState;
|
|
78
|
+
}
|
|
67
79
|
/** Fold a unit from init over one session's event log (the detached cold recipe). */
|
|
68
|
-
export declare function foldBillingUnit(unit:
|
|
80
|
+
export declare function foldBillingUnit(unit: BillingUnitFold, events: readonly SessionEvent[]): BillingUnitState;
|
|
69
81
|
/**
|
|
70
82
|
* Fold a unit from init over one session's OWN events only: the complete log
|
|
71
83
|
* minus its inherited fork prefix (`seq < seedLength`). A forked child's
|
|
@@ -78,5 +90,5 @@ export declare function foldBillingUnit(unit: Pick<ProjectionDefinition<'billing
|
|
|
78
90
|
* ({@link forkBoundaryOf}); 0 for an unseeded session.
|
|
79
91
|
* @returns the unit state folded over the session's own events.
|
|
80
92
|
*/
|
|
81
|
-
export declare function foldOwnBilling(unit:
|
|
93
|
+
export declare function foldOwnBilling(unit: BillingUnitFold, events: readonly SessionEvent[], seedLength?: number): BillingUnitState;
|
|
82
94
|
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -20,22 +20,29 @@
|
|
|
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';
|
|
34
42
|
import type { SessionPersistenceRevision } from '@deepseek-ai/dsh-session-persistence';
|
|
35
43
|
import type { ResolvedBilling } from './billing.ts';
|
|
36
44
|
import type { DeepSeekTodaySessionsSpend, DeepSeekTodaySpend } from './types.ts';
|
|
37
|
-
import { BILLING_UNIT_KEY, type BillingUnitState } from './projection.ts';
|
|
38
|
-
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
|
|
45
|
+
import { BILLING_UNIT_KEY, type BillingUnitFold, type BillingUnitState } from './projection.ts';
|
|
39
46
|
/**
|
|
40
47
|
* Fold one session's durable display title: the latest `session/title`
|
|
41
48
|
* event's text (last-wins, matching the `title` projection), or `null` before
|
|
@@ -45,21 +52,102 @@ import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
|
|
|
45
52
|
* @returns the session's current title, or `null` when untitled.
|
|
46
53
|
*/
|
|
47
54
|
export declare function foldSessionTitle(events: readonly SessionEvent[]): string | null;
|
|
48
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Structural slice of a live session the scanner reads, accepting both DSH
|
|
57
|
+
* runtime families: the ≤ 0.1.1-rc.2 baseline exposes the log as
|
|
58
|
+
* `events` (+ `header.seedLength`), while 0.1.2-alpha.4+ exposes
|
|
59
|
+
* `snapshotEvents()` and `inheritedEventCount` (and dropped the header field).
|
|
60
|
+
*/
|
|
49
61
|
export interface ScannerSession {
|
|
50
62
|
readonly id: SessionId;
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
/** ≤ 0.1.1-rc.2: the full event log snapshot. */
|
|
64
|
+
readonly events?: readonly SessionEvent[];
|
|
65
|
+
/** 0.1.2-alpha.4+: materialize an immutable log snapshot; no args = the full current log. */
|
|
66
|
+
snapshotEvents?(fromSeq?: number, toSeqExclusive?: number): readonly SessionEvent[];
|
|
67
|
+
/** 0.1.2-alpha.4+: the durable inherited-prefix length (0 for an unseeded session). */
|
|
68
|
+
readonly inheritedEventCount?: number;
|
|
69
|
+
/** Durable header slice: `seedLength` (older runtime) or `isSeeded` (newer runtime). */
|
|
53
70
|
readonly header?: {
|
|
54
71
|
readonly seedLength?: number;
|
|
72
|
+
readonly isSeeded?: boolean;
|
|
55
73
|
};
|
|
56
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Read one live session's complete event log across both runtime families.
|
|
77
|
+
* @throws when the session exposes neither the legacy `events` snapshot nor
|
|
78
|
+
* the newer `snapshotEvents()` reader — an unknown runtime surface must
|
|
79
|
+
* fail loudly rather than silently price an empty log.
|
|
80
|
+
*/
|
|
81
|
+
export declare function liveSessionEvents(session: ScannerSession): readonly SessionEvent[];
|
|
57
82
|
/** Structural slice of a listed persisted session (the snapshot header is a full SessionHeader). */
|
|
58
83
|
export interface ScannerPersistedHeader {
|
|
59
84
|
readonly id: SessionId;
|
|
60
|
-
/**
|
|
85
|
+
/** ≤ 0.1.1-rc.2: the durable fork boundary carried by the snapshot header; absent for an unseeded session. */
|
|
61
86
|
readonly seedLength?: number;
|
|
87
|
+
/** 0.1.2-alpha.4+: whether the session has a fork-inherited prefix (exact cut arrives with the inspect result). */
|
|
88
|
+
readonly isSeeded?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** One stored-session read: the full event log plus the durable inherited boundary. */
|
|
91
|
+
export interface ScannerPersistedRead {
|
|
92
|
+
readonly events: readonly SessionEvent[];
|
|
93
|
+
/** Inherited-prefix length (fork seed length); 0 for an unseeded session. */
|
|
94
|
+
readonly seedLength: number;
|
|
62
95
|
}
|
|
96
|
+
/** ≤ 0.1.1-rc.2 persistence slice: service-level `inspect` / `listSnapshots`. */
|
|
97
|
+
export interface ScannerPersistenceLegacy {
|
|
98
|
+
listSnapshots(): Promise<readonly {
|
|
99
|
+
header: ScannerPersistedHeader;
|
|
100
|
+
revision: SessionPersistenceRevision;
|
|
101
|
+
}[]>;
|
|
102
|
+
inspect(id: SessionId): Promise<{
|
|
103
|
+
meta?: {
|
|
104
|
+
readonly seedLength?: number;
|
|
105
|
+
readonly isSeeded?: boolean;
|
|
106
|
+
};
|
|
107
|
+
/** 0.1.2-alpha.4+: the exact inherited cut travels beside, not inside, the header. */
|
|
108
|
+
inheritedEventCount?: number;
|
|
109
|
+
events: readonly SessionEvent[];
|
|
110
|
+
}>;
|
|
111
|
+
}
|
|
112
|
+
/** 0.1.2-alpha.5+ handle-based persistence slice: service-level `list` / `open` + `SessionHandle`. */
|
|
113
|
+
export interface ScannerPersistenceHandle {
|
|
114
|
+
list(): Promise<readonly {
|
|
115
|
+
header: ScannerPersistedHeader;
|
|
116
|
+
revision: SessionPersistenceRevision;
|
|
117
|
+
}[]>;
|
|
118
|
+
open(id: SessionId, access: 'read'): Promise<{
|
|
119
|
+
readonly header?: {
|
|
120
|
+
readonly seedLength?: number;
|
|
121
|
+
readonly isSeeded?: boolean;
|
|
122
|
+
};
|
|
123
|
+
/** 0.1.2-alpha.5+: the handle carries the exact inherited cut beside the header. */
|
|
124
|
+
readonly inheritedEventCount?: number;
|
|
125
|
+
read(): Promise<readonly SessionEvent[]>;
|
|
126
|
+
close(): Promise<void>;
|
|
127
|
+
}>;
|
|
128
|
+
}
|
|
129
|
+
/** Structural union the scanner reads through, accepting both persistence runtime families. */
|
|
130
|
+
export type ScannerPersistence = ScannerPersistenceLegacy | ScannerPersistenceHandle;
|
|
131
|
+
/**
|
|
132
|
+
* List every stored session snapshot across both persistence runtime families:
|
|
133
|
+
* `listSnapshots` (≤ 0.1.1-rc.2) or `list` (0.1.2-alpha.5+).
|
|
134
|
+
* @param persistence - the persistence service slice.
|
|
135
|
+
* @returns one snapshot per stored session.
|
|
136
|
+
*/
|
|
137
|
+
export declare function persistenceListSnapshots(persistence: ScannerPersistence): Promise<readonly {
|
|
138
|
+
header: ScannerPersistedHeader;
|
|
139
|
+
revision: SessionPersistenceRevision;
|
|
140
|
+
}[]>;
|
|
141
|
+
/**
|
|
142
|
+
* Read one stored session's complete event log and durable inherited boundary
|
|
143
|
+
* across both persistence runtime families: legacy `inspect` (≤ 0.1.1-rc.2)
|
|
144
|
+
* or `open` + handle `read` (0.1.2-alpha.5+; the handle is closed after the
|
|
145
|
+
* read). Both throw when the session does not exist.
|
|
146
|
+
* @param persistence - the persistence service slice.
|
|
147
|
+
* @param id - the stored session to read.
|
|
148
|
+
* @returns the session's complete event log plus its inherited-prefix boundary.
|
|
149
|
+
*/
|
|
150
|
+
export declare function persistenceInspect(persistence: ScannerPersistence, id: SessionId): Promise<ScannerPersistedRead>;
|
|
63
151
|
/** Structural slices of the optional services the scanner reads through. */
|
|
64
152
|
export interface TodaySpendScannerDeps {
|
|
65
153
|
/** Resolves the live SessionStore at scan time (absent in headless assemblies). */
|
|
@@ -67,18 +155,7 @@ export interface TodaySpendScannerDeps {
|
|
|
67
155
|
list(): readonly ScannerSession[];
|
|
68
156
|
} | undefined;
|
|
69
157
|
/** Resolves the persistence backend at scan time (absent without persistence). */
|
|
70
|
-
persistence?: () =>
|
|
71
|
-
listSnapshots(): Promise<readonly {
|
|
72
|
-
header: ScannerPersistedHeader;
|
|
73
|
-
revision: SessionPersistenceRevision;
|
|
74
|
-
}[]>;
|
|
75
|
-
inspect(id: SessionId): Promise<{
|
|
76
|
-
meta: {
|
|
77
|
-
seedLength?: number;
|
|
78
|
-
};
|
|
79
|
-
events: readonly SessionEvent[];
|
|
80
|
-
}>;
|
|
81
|
-
} | undefined;
|
|
158
|
+
persistence?: () => ScannerPersistence | undefined;
|
|
82
159
|
/** Resolves the session-projection registry at scan time (absent → events path). */
|
|
83
160
|
projections?: () => {
|
|
84
161
|
stateOf(session: ScannerSession, key: typeof BILLING_UNIT_KEY): BillingUnitState | undefined;
|
|
@@ -97,7 +174,7 @@ export interface TodaySpendScannerDeps {
|
|
|
97
174
|
*/
|
|
98
175
|
ensureUnit?: () => void;
|
|
99
176
|
/** The billing unit's fold (the projection path's detached cold recipe). */
|
|
100
|
-
unit:
|
|
177
|
+
unit: BillingUnitFold;
|
|
101
178
|
/** Hard cap on today's events collected by the events path. */
|
|
102
179
|
maxEvents: number;
|
|
103
180
|
/** Warn sink for truncation and unreadable sessions. */
|
|
@@ -179,9 +256,12 @@ export declare class TodaySpendScanner {
|
|
|
179
256
|
* until the session is inspected again. A SEEDED session (fork child)
|
|
180
257
|
* skips the ladder entirely: its cached row was folded over the inherited
|
|
181
258
|
* prefix too, so it always detaches through inspect with the durable
|
|
182
|
-
* boundary
|
|
259
|
+
* boundary (the inspect result's inherited count or `meta.seedLength`,
|
|
260
|
+
* depending on the runtime family) applied to the local fold.
|
|
183
261
|
* @param id - the cold session's id.
|
|
184
|
-
* @param
|
|
262
|
+
* @param seeded - whether the session carries a fork-inherited prefix
|
|
263
|
+
* (from the snapshot header: `isSeeded` on 0.1.2-alpha.4+, `seedLength`
|
|
264
|
+
* at and before the 0.1.1-rc.2 baseline).
|
|
185
265
|
* @returns the resolved state and title, or `undefined` when unreadable.
|
|
186
266
|
*/
|
|
187
267
|
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,56 @@ 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
|
+
}
|
|
78
|
+
function isHandlePersistence(persistence) {
|
|
79
|
+
return typeof persistence.open === 'function';
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* List every stored session snapshot across both persistence runtime families:
|
|
83
|
+
* `listSnapshots` (≤ 0.1.1-rc.2) or `list` (0.1.2-alpha.5+).
|
|
84
|
+
* @param persistence - the persistence service slice.
|
|
85
|
+
* @returns one snapshot per stored session.
|
|
86
|
+
*/
|
|
87
|
+
export function persistenceListSnapshots(persistence) {
|
|
88
|
+
return isHandlePersistence(persistence)
|
|
89
|
+
? persistence.list()
|
|
90
|
+
: persistence.listSnapshots();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Read one stored session's complete event log and durable inherited boundary
|
|
94
|
+
* across both persistence runtime families: legacy `inspect` (≤ 0.1.1-rc.2)
|
|
95
|
+
* or `open` + handle `read` (0.1.2-alpha.5+; the handle is closed after the
|
|
96
|
+
* read). Both throw when the session does not exist.
|
|
97
|
+
* @param persistence - the persistence service slice.
|
|
98
|
+
* @param id - the stored session to read.
|
|
99
|
+
* @returns the session's complete event log plus its inherited-prefix boundary.
|
|
100
|
+
*/
|
|
101
|
+
export async function persistenceInspect(persistence, id) {
|
|
102
|
+
if (isHandlePersistence(persistence)) {
|
|
103
|
+
const handle = await persistence.open(id, 'read');
|
|
104
|
+
try {
|
|
105
|
+
return { events: await handle.read(), seedLength: forkBoundaryOf(handle) };
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await handle.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const inspection = await persistence.inspect(id);
|
|
112
|
+
return { events: inspection.events, seedLength: forkBoundaryOf(inspection) };
|
|
113
|
+
}
|
|
56
114
|
/**
|
|
57
115
|
* Bounded parallel fan-out: run `run` over `items` with at most `limit` in
|
|
58
116
|
* flight. A shared index counter hands each worker its next job, so the
|
|
@@ -176,17 +234,20 @@ export class TodaySpendScanner {
|
|
|
176
234
|
* until the session is inspected again. A SEEDED session (fork child)
|
|
177
235
|
* skips the ladder entirely: its cached row was folded over the inherited
|
|
178
236
|
* prefix too, so it always detaches through inspect with the durable
|
|
179
|
-
* boundary
|
|
237
|
+
* boundary (the inspect result's inherited count or `meta.seedLength`,
|
|
238
|
+
* depending on the runtime family) applied to the local fold.
|
|
180
239
|
* @param id - the cold session's id.
|
|
181
|
-
* @param
|
|
240
|
+
* @param seeded - whether the session carries a fork-inherited prefix
|
|
241
|
+
* (from the snapshot header: `isSeeded` on 0.1.2-alpha.4+, `seedLength`
|
|
242
|
+
* at and before the 0.1.1-rc.2 baseline).
|
|
182
243
|
* @returns the resolved state and title, or `undefined` when unreadable.
|
|
183
244
|
*/
|
|
184
|
-
async resolveCold(id,
|
|
185
|
-
const { persistence, projectionCache,
|
|
245
|
+
async resolveCold(id, seeded) {
|
|
246
|
+
const { persistence, projectionCache, logger } = this.deps;
|
|
186
247
|
const persistenceService = persistence?.();
|
|
187
248
|
if (persistenceService === undefined)
|
|
188
249
|
return undefined;
|
|
189
|
-
if (
|
|
250
|
+
if (!seeded) {
|
|
190
251
|
const cache = projectionCache?.();
|
|
191
252
|
if (cache !== undefined) {
|
|
192
253
|
try {
|
|
@@ -200,8 +261,11 @@ export class TodaySpendScanner {
|
|
|
200
261
|
}
|
|
201
262
|
}
|
|
202
263
|
try {
|
|
203
|
-
const
|
|
204
|
-
return {
|
|
264
|
+
const read = await persistenceInspect(persistenceService, id);
|
|
265
|
+
return {
|
|
266
|
+
value: foldOwnBilling(this.deps.unit, read.events, read.seedLength),
|
|
267
|
+
title: foldSessionTitle(read.events),
|
|
268
|
+
};
|
|
205
269
|
}
|
|
206
270
|
catch (error) {
|
|
207
271
|
// One unreadable session must not blank the whole-day aggregate.
|
|
@@ -250,11 +314,11 @@ export class TodaySpendScanner {
|
|
|
250
314
|
if (store !== undefined) {
|
|
251
315
|
for (const session of store.list()) {
|
|
252
316
|
liveIds.add(session.id);
|
|
253
|
-
const seedLength = forkBoundaryOf(session
|
|
317
|
+
const seedLength = forkBoundaryOf(session);
|
|
254
318
|
// A fork child's eager cell covers its inherited prefix too; price
|
|
255
319
|
// its own events directly instead.
|
|
256
320
|
const state = seedLength > 0
|
|
257
|
-
? this.ownBillingState(session.id, session
|
|
321
|
+
? this.ownBillingState(session.id, liveSessionEvents(session), seedLength)
|
|
258
322
|
: projectionsService?.stateOf(session, BILLING_UNIT_KEY);
|
|
259
323
|
if (state !== undefined && state.dayKey === dayKey) {
|
|
260
324
|
total = mergeTodaySpend(total, state.spend);
|
|
@@ -265,22 +329,22 @@ export class TodaySpendScanner {
|
|
|
265
329
|
const persistenceService = persistence?.();
|
|
266
330
|
if (persistenceService === undefined)
|
|
267
331
|
return total;
|
|
268
|
-
const snapshots = await persistenceService
|
|
332
|
+
const snapshots = await persistenceListSnapshots(persistenceService);
|
|
269
333
|
const pending = [];
|
|
270
334
|
for (const { header, revision } of snapshots) {
|
|
271
335
|
if (liveIds.has(header.id))
|
|
272
336
|
continue;
|
|
273
|
-
const
|
|
337
|
+
const seeded = isSeededSession(header);
|
|
274
338
|
const resolved = this.coldResolved.get(header.id);
|
|
275
339
|
if (resolved !== undefined && resolved.revision === revision) {
|
|
276
340
|
if (resolved.value.dayKey === dayKey)
|
|
277
341
|
total = mergeTodaySpend(total, resolved.value.spend);
|
|
278
342
|
continue;
|
|
279
343
|
}
|
|
280
|
-
pending.push({ id: header.id, revision,
|
|
344
|
+
pending.push({ id: header.id, revision, seeded });
|
|
281
345
|
}
|
|
282
|
-
await withConcurrency(pending, 8, async ({ id, revision,
|
|
283
|
-
const resolved = await this.resolveCold(id,
|
|
346
|
+
await withConcurrency(pending, 8, async ({ id, revision, seeded }) => {
|
|
347
|
+
const resolved = await this.resolveCold(id, seeded);
|
|
284
348
|
if (resolved !== undefined)
|
|
285
349
|
this.coldResolved.set(id, { revision, ...resolved });
|
|
286
350
|
});
|
|
@@ -326,7 +390,7 @@ export class TodaySpendScanner {
|
|
|
326
390
|
if (store !== undefined) {
|
|
327
391
|
for (const session of store.list()) {
|
|
328
392
|
liveIds.add(session.id);
|
|
329
|
-
collect(session
|
|
393
|
+
collect(liveSessionEvents(session), forkBoundaryOf(session));
|
|
330
394
|
if (truncated)
|
|
331
395
|
break;
|
|
332
396
|
}
|
|
@@ -334,15 +398,15 @@ export class TodaySpendScanner {
|
|
|
334
398
|
}
|
|
335
399
|
const persistenceService = persistence?.();
|
|
336
400
|
if (!truncated && persistenceService !== undefined) {
|
|
337
|
-
const snapshots = await persistenceService
|
|
401
|
+
const snapshots = await persistenceListSnapshots(persistenceService);
|
|
338
402
|
for (const { header, revision } of snapshots) {
|
|
339
403
|
if (liveIds.has(header.id))
|
|
340
404
|
continue;
|
|
341
405
|
if (this.lastEventsScan?.get(header.id) === revision)
|
|
342
406
|
continue;
|
|
343
407
|
try {
|
|
344
|
-
const
|
|
345
|
-
collect(
|
|
408
|
+
const read = await persistenceInspect(persistenceService, header.id);
|
|
409
|
+
collect(read.events, read.seedLength);
|
|
346
410
|
}
|
|
347
411
|
catch (error) {
|
|
348
412
|
// One unreadable session must not blank the whole-day aggregate.
|
|
@@ -381,14 +445,15 @@ export class TodaySpendScanner {
|
|
|
381
445
|
if (store !== undefined) {
|
|
382
446
|
for (const session of store.list()) {
|
|
383
447
|
liveIds.add(session.id);
|
|
384
|
-
const seedLength = forkBoundaryOf(session
|
|
448
|
+
const seedLength = forkBoundaryOf(session);
|
|
449
|
+
const events = liveSessionEvents(session);
|
|
385
450
|
const state = seedLength > 0
|
|
386
|
-
? this.ownBillingState(session.id,
|
|
451
|
+
? this.ownBillingState(session.id, events, seedLength)
|
|
387
452
|
: projectionsService?.stateOf(session, BILLING_UNIT_KEY);
|
|
388
453
|
if (state !== undefined && state.dayKey === dayKey) {
|
|
389
454
|
rows.set(session.id, {
|
|
390
455
|
sessionId: session.id,
|
|
391
|
-
title: foldSessionTitle(
|
|
456
|
+
title: foldSessionTitle(events),
|
|
392
457
|
total: state.spend.total,
|
|
393
458
|
});
|
|
394
459
|
}
|
|
@@ -398,12 +463,12 @@ export class TodaySpendScanner {
|
|
|
398
463
|
const persistenceService = persistence?.();
|
|
399
464
|
if (persistenceService === undefined)
|
|
400
465
|
return [...rows.values()];
|
|
401
|
-
const snapshots = await persistenceService
|
|
466
|
+
const snapshots = await persistenceListSnapshots(persistenceService);
|
|
402
467
|
const pending = [];
|
|
403
468
|
for (const { header, revision } of snapshots) {
|
|
404
469
|
if (liveIds.has(header.id))
|
|
405
470
|
continue;
|
|
406
|
-
const
|
|
471
|
+
const seeded = isSeededSession(header);
|
|
407
472
|
const resolved = this.coldResolved.get(header.id);
|
|
408
473
|
if (resolved !== undefined && resolved.revision === revision) {
|
|
409
474
|
if (resolved.value.dayKey === dayKey) {
|
|
@@ -411,10 +476,10 @@ export class TodaySpendScanner {
|
|
|
411
476
|
}
|
|
412
477
|
continue;
|
|
413
478
|
}
|
|
414
|
-
pending.push({ id: header.id, revision,
|
|
479
|
+
pending.push({ id: header.id, revision, seeded });
|
|
415
480
|
}
|
|
416
|
-
await withConcurrency(pending, 8, async ({ id, revision,
|
|
417
|
-
const resolved = await this.resolveCold(id,
|
|
481
|
+
await withConcurrency(pending, 8, async ({ id, revision, seeded }) => {
|
|
482
|
+
const resolved = await this.resolveCold(id, seeded);
|
|
418
483
|
if (resolved !== undefined)
|
|
419
484
|
this.coldResolved.set(id, { revision, ...resolved });
|
|
420
485
|
});
|
|
@@ -470,7 +535,7 @@ export class TodaySpendScanner {
|
|
|
470
535
|
if (store !== undefined) {
|
|
471
536
|
for (const session of store.list()) {
|
|
472
537
|
liveIds.add(session.id);
|
|
473
|
-
collect(session.id, session
|
|
538
|
+
collect(session.id, liveSessionEvents(session), forkBoundaryOf(session));
|
|
474
539
|
if (truncated)
|
|
475
540
|
break;
|
|
476
541
|
}
|
|
@@ -478,15 +543,15 @@ export class TodaySpendScanner {
|
|
|
478
543
|
}
|
|
479
544
|
const persistenceService = persistence?.();
|
|
480
545
|
if (!truncated && persistenceService !== undefined) {
|
|
481
|
-
const snapshots = await persistenceService
|
|
546
|
+
const snapshots = await persistenceListSnapshots(persistenceService);
|
|
482
547
|
for (const { header, revision } of snapshots) {
|
|
483
548
|
if (liveIds.has(header.id))
|
|
484
549
|
continue;
|
|
485
550
|
if (this.lastEventsScan?.get(header.id) === revision)
|
|
486
551
|
continue;
|
|
487
552
|
try {
|
|
488
|
-
const
|
|
489
|
-
collect(header.id,
|
|
553
|
+
const read = await persistenceInspect(persistenceService, header.id);
|
|
554
|
+
collect(header.id, read.events, read.seedLength);
|
|
490
555
|
}
|
|
491
556
|
catch (error) {
|
|
492
557
|
// One unreadable session must not blank the whole-day aggregate.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rayadesu/dsh-llm-billing",
|
|
3
3
|
"description": "Standalone DeepSeek account-balance and session-spend provider exposed through the billing Remote",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.7",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -52,14 +52,14 @@
|
|
|
52
52
|
},
|
|
53
53
|
"license": "MIT",
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@deepseek-ai/dsh-credentials": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-launch-environment": "^0.1.
|
|
57
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
58
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
59
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
60
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.
|
|
61
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.
|
|
62
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.
|
|
55
|
+
"@deepseek-ai/dsh-credentials": "^0.1.2-alpha.5",
|
|
56
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.2-alpha.5",
|
|
57
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.5",
|
|
58
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.5",
|
|
59
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
|
|
60
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.5",
|
|
61
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.5",
|
|
62
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.5",
|
|
63
63
|
"@deepseek-ai/cordis": "^4.0.1"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
@@ -67,14 +67,14 @@
|
|
|
67
67
|
"zod": "^4.4.3"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@deepseek-ai/dsh-credentials": "^0.1.
|
|
71
|
-
"@deepseek-ai/dsh-launch-environment": "^0.1.
|
|
72
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
73
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
74
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
75
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.
|
|
76
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.
|
|
77
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.
|
|
70
|
+
"@deepseek-ai/dsh-credentials": "^0.1.2-alpha.5",
|
|
71
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.2-alpha.5",
|
|
72
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.5",
|
|
73
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.5",
|
|
74
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
|
|
75
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.5",
|
|
76
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.5",
|
|
77
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.5",
|
|
78
78
|
"@deepseek-ai/cordis": "^4.0.1"
|
|
79
79
|
}
|
|
80
80
|
}
|