@rayadesu/dsh-llm-billing 0.3.6 → 0.3.8

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 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: 85c6cc052cd3332f23351db8123259565a669602
6
- README.zh.md: 96d0d9d70fe94bfb0afa2e2af3f97231103d3590
5
+ README.md: 3c0ae19286d6935190659ec1e1c93b7b63ab5c14
6
+ README.zh.md: 08adb0d018b376fb5fa995b29cbb6790063f4814
package/README.md CHANGED
@@ -35,7 +35,9 @@ A forked session (DSH's "fork" of a conversation) opens its log with a verbatim
35
35
 
36
36
  ## Runtime compatibility
37
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.
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.
39
41
 
40
42
  ## Configuration
41
43
 
package/README.zh.md CHANGED
@@ -35,7 +35,9 @@
35
35
 
36
36
  ## 运行时兼容性
37
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 运行时代码均无需改动即可工作。遇到两种形状都没有的未知运行时表面时,插件会显式失败而不是静默按零花费计价。
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。
39
41
 
40
42
  ## 配置
41
43
 
package/lib/index.js CHANGED
@@ -337,6 +337,32 @@ const DEFAULT_MODEL_PRICING = [
337
337
  cacheMissInput: 1.5,
338
338
  output: 4.5
339
339
  }
340
+ },
341
+ {
342
+ model: "mimo-v2.5-pro",
343
+ peak: {
344
+ cacheHitInput: .025,
345
+ cacheMissInput: 3,
346
+ output: 6
347
+ },
348
+ offPeak: {
349
+ cacheHitInput: .025,
350
+ cacheMissInput: 3,
351
+ output: 6
352
+ }
353
+ },
354
+ {
355
+ model: "mimo-v2.5",
356
+ peak: {
357
+ cacheHitInput: .02,
358
+ cacheMissInput: 1,
359
+ output: 2
360
+ },
361
+ offPeak: {
362
+ cacheHitInput: .02,
363
+ cacheMissInput: 1,
364
+ output: 2
365
+ }
340
366
  }
341
367
  ];
342
368
  /**
@@ -836,6 +862,45 @@ function liveSessionEvents(session) {
836
862
  if (session.snapshotEvents !== void 0) return session.snapshotEvents();
837
863
  throw new Error("llm-billing: session log surface is neither Session.events nor Session.snapshotEvents");
838
864
  }
865
+ function isHandlePersistence(persistence) {
866
+ return typeof persistence.open === "function";
867
+ }
868
+ /**
869
+ * List every stored session snapshot across both persistence runtime families:
870
+ * `listSnapshots` (≤ 0.1.1-rc.2) or `list` (0.1.2-alpha.5+).
871
+ * @param persistence - the persistence service slice.
872
+ * @returns one snapshot per stored session.
873
+ */
874
+ function persistenceListSnapshots(persistence) {
875
+ return isHandlePersistence(persistence) ? persistence.list() : persistence.listSnapshots();
876
+ }
877
+ /**
878
+ * Read one stored session's complete event log and durable inherited boundary
879
+ * across both persistence runtime families: legacy `inspect` (≤ 0.1.1-rc.2)
880
+ * or `open` + handle `read` (0.1.2-alpha.5+; the handle is closed after the
881
+ * read). Both throw when the session does not exist.
882
+ * @param persistence - the persistence service slice.
883
+ * @param id - the stored session to read.
884
+ * @returns the session's complete event log plus its inherited-prefix boundary.
885
+ */
886
+ async function persistenceInspect(persistence, id) {
887
+ if (isHandlePersistence(persistence)) {
888
+ const handle = await persistence.open(id, "read");
889
+ try {
890
+ return {
891
+ events: await handle.read(),
892
+ seedLength: forkBoundaryOf(handle)
893
+ };
894
+ } finally {
895
+ await handle.close();
896
+ }
897
+ }
898
+ const inspection = await persistence.inspect(id);
899
+ return {
900
+ events: inspection.events,
901
+ seedLength: forkBoundaryOf(inspection)
902
+ };
903
+ }
839
904
  /**
840
905
  * Bounded parallel fan-out: run `run` over `items` with at most `limit` in
841
906
  * flight. A shared index counter hands each worker its next job, so the
@@ -975,10 +1040,10 @@ var TodaySpendScanner = class {
975
1040
  }
976
1041
  }
977
1042
  try {
978
- const inspection = await persistenceService.inspect(id);
1043
+ const read = await persistenceInspect(persistenceService, id);
979
1044
  return {
980
- value: foldOwnBilling(this.deps.unit, inspection.events, forkBoundaryOf(inspection)),
981
- title: foldSessionTitle(inspection.events)
1045
+ value: foldOwnBilling(this.deps.unit, read.events, read.seedLength),
1046
+ title: foldSessionTitle(read.events)
982
1047
  };
983
1048
  } catch (error) {
984
1049
  logger.warn(`llm-billing: skipping unreadable session ${id}: ${String(error)}`);
@@ -1029,7 +1094,7 @@ var TodaySpendScanner = class {
1029
1094
  }
1030
1095
  const persistenceService = persistence?.();
1031
1096
  if (persistenceService === void 0) return total;
1032
- const snapshots = await persistenceService.listSnapshots();
1097
+ const snapshots = await persistenceListSnapshots(persistenceService);
1033
1098
  const pending = [];
1034
1099
  for (const { header, revision } of snapshots) {
1035
1100
  if (liveIds.has(header.id)) continue;
@@ -1094,13 +1159,13 @@ var TodaySpendScanner = class {
1094
1159
  }
1095
1160
  const persistenceService = persistence?.();
1096
1161
  if (!truncated && persistenceService !== void 0) {
1097
- const snapshots = await persistenceService.listSnapshots();
1162
+ const snapshots = await persistenceListSnapshots(persistenceService);
1098
1163
  for (const { header, revision } of snapshots) {
1099
1164
  if (liveIds.has(header.id)) continue;
1100
1165
  if (this.lastEventsScan?.get(header.id) === revision) continue;
1101
1166
  try {
1102
- const inspection = await persistenceService.inspect(header.id);
1103
- collect(inspection.events, forkBoundaryOf(inspection));
1167
+ const read = await persistenceInspect(persistenceService, header.id);
1168
+ collect(read.events, read.seedLength);
1104
1169
  } catch (error) {
1105
1170
  logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
1106
1171
  }
@@ -1141,7 +1206,7 @@ var TodaySpendScanner = class {
1141
1206
  }
1142
1207
  const persistenceService = persistence?.();
1143
1208
  if (persistenceService === void 0) return [...rows.values()];
1144
- const snapshots = await persistenceService.listSnapshots();
1209
+ const snapshots = await persistenceListSnapshots(persistenceService);
1145
1210
  const pending = [];
1146
1211
  for (const { header, revision } of snapshots) {
1147
1212
  if (liveIds.has(header.id)) continue;
@@ -1227,13 +1292,13 @@ var TodaySpendScanner = class {
1227
1292
  }
1228
1293
  const persistenceService = persistence?.();
1229
1294
  if (!truncated && persistenceService !== void 0) {
1230
- const snapshots = await persistenceService.listSnapshots();
1295
+ const snapshots = await persistenceListSnapshots(persistenceService);
1231
1296
  for (const { header, revision } of snapshots) {
1232
1297
  if (liveIds.has(header.id)) continue;
1233
1298
  if (this.lastEventsScan?.get(header.id) === revision) continue;
1234
1299
  try {
1235
- const inspection = await persistenceService.inspect(header.id);
1236
- collect(header.id, inspection.events, forkBoundaryOf(inspection));
1300
+ const read = await persistenceInspect(persistenceService, header.id);
1301
+ collect(header.id, read.events, read.seedLength);
1237
1302
  } catch (error) {
1238
1303
  logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
1239
1304
  }
@@ -1290,6 +1355,14 @@ const DEFAULT_MODELS = [
1290
1355
  {
1291
1356
  id: "deepseek-v4-flash-vision-exp",
1292
1357
  name: "DeepSeek-V4-Flash-Vision-Exp"
1358
+ },
1359
+ {
1360
+ id: "mimo-v2.5-pro",
1361
+ name: "MiMo-V2.5-Pro"
1362
+ },
1363
+ {
1364
+ id: "mimo-v2.5",
1365
+ name: "MiMo-V2.5"
1293
1366
  }
1294
1367
  ];
1295
1368
  const billingModel = z.object({
@@ -1342,11 +1415,7 @@ async function sessionEvents(ctx, sessionId) {
1342
1415
  };
1343
1416
  const persistence = ctx.get("sessionPersistence");
1344
1417
  if (persistence !== void 0) try {
1345
- const inspection = await persistence.inspect(sessionId);
1346
- return {
1347
- events: inspection.events,
1348
- seedLength: forkBoundaryOf(inspection)
1349
- };
1418
+ return await persistenceInspect(persistence, sessionId);
1350
1419
  } catch (error) {
1351
1420
  throw new LlmError(`llm-billing: session ${sessionId} not found`, "NOT_FOUND", { cause: error });
1352
1421
  }
@@ -1440,4 +1509,4 @@ function apply(ctx, config) {
1440
1509
  });
1441
1510
  }
1442
1511
  //#endregion
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 };
1512
+ 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 };
@@ -110,10 +110,12 @@ export interface ForkBoundarySource {
110
110
  /** ≤ 0.1.1-rc.2: the header slice of a `Session` (its `header.seedLength`). */
111
111
  readonly header?: {
112
112
  readonly seedLength?: number;
113
+ readonly isSeeded?: boolean;
113
114
  };
114
115
  /** ≤ 0.1.1-rc.2: the `meta` of a persistence `inspect` result (its `seedLength`). */
115
116
  readonly meta?: {
116
117
  readonly seedLength?: number;
118
+ readonly isSeeded?: boolean;
117
119
  };
118
120
  }
119
121
  /**
@@ -40,6 +40,17 @@ export const DEFAULT_MODEL_PRICING = [
40
40
  peak: { cacheHitInput: 0.10, cacheMissInput: 3.0, output: 9.0 },
41
41
  offPeak: { cacheHitInput: 0.05, cacheMissInput: 1.5, output: 4.5 },
42
42
  },
43
+ // MiMo-V2.5 series (Xiaomi): flat rate, no peak/off-peak distinction.
44
+ {
45
+ model: 'mimo-v2.5-pro',
46
+ peak: { cacheHitInput: 0.025, cacheMissInput: 3.0, output: 6.0 },
47
+ offPeak: { cacheHitInput: 0.025, cacheMissInput: 3.0, output: 6.0 },
48
+ },
49
+ {
50
+ model: 'mimo-v2.5',
51
+ peak: { cacheHitInput: 0.02, cacheMissInput: 1.0, output: 2.0 },
52
+ offPeak: { cacheHitInput: 0.02, cacheMissInput: 1.0, output: 2.0 },
53
+ },
43
54
  ];
44
55
  /**
45
56
  * Resolve optional configuration to a pricing table, defaulting omitted or
@@ -28,9 +28,9 @@ export { addEventContribution, beijingDayKey, computeSessionSpend, computeTodayS
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, liveSessionEvents, 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";
@@ -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 { liveSessionEvents, 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
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, liveSessionEvents, 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';
@@ -41,6 +41,8 @@ const DEFAULT_MODELS = [
41
41
  { id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash' },
42
42
  { id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' },
43
43
  { id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp' },
44
+ { id: 'mimo-v2.5-pro', name: 'MiMo-V2.5-Pro' },
45
+ { id: 'mimo-v2.5', name: 'MiMo-V2.5' },
44
46
  ];
45
47
  const billingModel = z.object({
46
48
  id: z.string().required(),
@@ -93,8 +95,7 @@ async function sessionEvents(ctx, sessionId) {
93
95
  const persistence = ctx.get('sessionPersistence');
94
96
  if (persistence !== undefined) {
95
97
  try {
96
- const inspection = await persistence.inspect(sessionId);
97
- return { events: inspection.events, seedLength: forkBoundaryOf(inspection) };
98
+ return await persistenceInspect(persistence, sessionId);
98
99
  }
99
100
  catch (error) {
100
101
  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: Pick<ProjectionDefinition<'billingTodaySpend', BillingUnitState>, 'init' | 'apply'>, events: readonly SessionEvent[]): BillingUnitState;
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: Pick<ProjectionDefinition<'billingTodaySpend', BillingUnitState>, 'init' | 'apply'>, events: readonly SessionEvent[], seedLength?: number): BillingUnitState;
93
+ export declare function foldOwnBilling(unit: BillingUnitFold, events: readonly SessionEvent[], seedLength?: number): BillingUnitState;
82
94
  //# sourceMappingURL=projection.d.ts.map
@@ -42,8 +42,7 @@ import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session';
42
42
  import type { SessionPersistenceRevision } from '@deepseek-ai/dsh-session-persistence';
43
43
  import type { ResolvedBilling } from './billing.ts';
44
44
  import type { DeepSeekTodaySessionsSpend, DeepSeekTodaySpend } from './types.ts';
45
- import { BILLING_UNIT_KEY, type BillingUnitState } from './projection.ts';
46
- import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
45
+ import { BILLING_UNIT_KEY, type BillingUnitFold, type BillingUnitState } from './projection.ts';
47
46
  /**
48
47
  * Fold one session's durable display title: the latest `session/title`
49
48
  * event's text (last-wins, matching the `title` projection), or `null` before
@@ -64,7 +63,7 @@ export interface ScannerSession {
64
63
  /** ≤ 0.1.1-rc.2: the full event log snapshot. */
65
64
  readonly events?: readonly SessionEvent[];
66
65
  /** 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[];
66
+ snapshotEvents?(fromSeq?: number, toSeqExclusive?: number): readonly SessionEvent[];
68
67
  /** 0.1.2-alpha.4+: the durable inherited-prefix length (0 for an unseeded session). */
69
68
  readonly inheritedEventCount?: number;
70
69
  /** Durable header slice: `seedLength` (older runtime) or `isSeeded` (newer runtime). */
@@ -88,6 +87,67 @@ export interface ScannerPersistedHeader {
88
87
  /** 0.1.2-alpha.4+: whether the session has a fork-inherited prefix (exact cut arrives with the inspect result). */
89
88
  readonly isSeeded?: boolean;
90
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;
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>;
91
151
  /** Structural slices of the optional services the scanner reads through. */
92
152
  export interface TodaySpendScannerDeps {
93
153
  /** Resolves the live SessionStore at scan time (absent in headless assemblies). */
@@ -95,21 +155,7 @@ export interface TodaySpendScannerDeps {
95
155
  list(): readonly ScannerSession[];
96
156
  } | undefined;
97
157
  /** Resolves the persistence backend at scan time (absent without persistence). */
98
- persistence?: () => {
99
- listSnapshots(): Promise<readonly {
100
- header: ScannerPersistedHeader;
101
- revision: SessionPersistenceRevision;
102
- }[]>;
103
- inspect(id: SessionId): Promise<{
104
- meta?: {
105
- readonly seedLength?: number;
106
- readonly isSeeded?: boolean;
107
- };
108
- /** 0.1.2-alpha.4+: the exact inherited cut travels beside, not inside, the header. */
109
- inheritedEventCount?: number;
110
- events: readonly SessionEvent[];
111
- }>;
112
- } | undefined;
158
+ persistence?: () => ScannerPersistence | undefined;
113
159
  /** Resolves the session-projection registry at scan time (absent → events path). */
114
160
  projections?: () => {
115
161
  stateOf(session: ScannerSession, key: typeof BILLING_UNIT_KEY): BillingUnitState | undefined;
@@ -128,7 +174,7 @@ export interface TodaySpendScannerDeps {
128
174
  */
129
175
  ensureUnit?: () => void;
130
176
  /** The billing unit's fold (the projection path's detached cold recipe). */
131
- unit: Pick<ProjectionDefinition<'billingTodaySpend', BillingUnitState>, 'init' | 'apply'>;
177
+ unit: BillingUnitFold;
132
178
  /** Hard cap on today's events collected by the events path. */
133
179
  maxEvents: number;
134
180
  /** Warn sink for truncation and unreadable sessions. */
@@ -75,6 +75,42 @@ export function liveSessionEvents(session) {
75
75
  return session.snapshotEvents();
76
76
  throw new Error('llm-billing: session log surface is neither Session.events nor Session.snapshotEvents');
77
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
+ }
78
114
  /**
79
115
  * Bounded parallel fan-out: run `run` over `items` with at most `limit` in
80
116
  * flight. A shared index counter hands each worker its next job, so the
@@ -225,10 +261,10 @@ export class TodaySpendScanner {
225
261
  }
226
262
  }
227
263
  try {
228
- const inspection = await persistenceService.inspect(id);
264
+ const read = await persistenceInspect(persistenceService, id);
229
265
  return {
230
- value: foldOwnBilling(this.deps.unit, inspection.events, forkBoundaryOf(inspection)),
231
- title: foldSessionTitle(inspection.events),
266
+ value: foldOwnBilling(this.deps.unit, read.events, read.seedLength),
267
+ title: foldSessionTitle(read.events),
232
268
  };
233
269
  }
234
270
  catch (error) {
@@ -293,7 +329,7 @@ export class TodaySpendScanner {
293
329
  const persistenceService = persistence?.();
294
330
  if (persistenceService === undefined)
295
331
  return total;
296
- const snapshots = await persistenceService.listSnapshots();
332
+ const snapshots = await persistenceListSnapshots(persistenceService);
297
333
  const pending = [];
298
334
  for (const { header, revision } of snapshots) {
299
335
  if (liveIds.has(header.id))
@@ -362,15 +398,15 @@ export class TodaySpendScanner {
362
398
  }
363
399
  const persistenceService = persistence?.();
364
400
  if (!truncated && persistenceService !== undefined) {
365
- const snapshots = await persistenceService.listSnapshots();
401
+ const snapshots = await persistenceListSnapshots(persistenceService);
366
402
  for (const { header, revision } of snapshots) {
367
403
  if (liveIds.has(header.id))
368
404
  continue;
369
405
  if (this.lastEventsScan?.get(header.id) === revision)
370
406
  continue;
371
407
  try {
372
- const inspection = await persistenceService.inspect(header.id);
373
- collect(inspection.events, forkBoundaryOf(inspection));
408
+ const read = await persistenceInspect(persistenceService, header.id);
409
+ collect(read.events, read.seedLength);
374
410
  }
375
411
  catch (error) {
376
412
  // One unreadable session must not blank the whole-day aggregate.
@@ -427,7 +463,7 @@ export class TodaySpendScanner {
427
463
  const persistenceService = persistence?.();
428
464
  if (persistenceService === undefined)
429
465
  return [...rows.values()];
430
- const snapshots = await persistenceService.listSnapshots();
466
+ const snapshots = await persistenceListSnapshots(persistenceService);
431
467
  const pending = [];
432
468
  for (const { header, revision } of snapshots) {
433
469
  if (liveIds.has(header.id))
@@ -507,15 +543,15 @@ export class TodaySpendScanner {
507
543
  }
508
544
  const persistenceService = persistence?.();
509
545
  if (!truncated && persistenceService !== undefined) {
510
- const snapshots = await persistenceService.listSnapshots();
546
+ const snapshots = await persistenceListSnapshots(persistenceService);
511
547
  for (const { header, revision } of snapshots) {
512
548
  if (liveIds.has(header.id))
513
549
  continue;
514
550
  if (this.lastEventsScan?.get(header.id) === revision)
515
551
  continue;
516
552
  try {
517
- const inspection = await persistenceService.inspect(header.id);
518
- collect(header.id, inspection.events, forkBoundaryOf(inspection));
553
+ const read = await persistenceInspect(persistenceService, header.id);
554
+ collect(header.id, read.events, read.seedLength);
519
555
  }
520
556
  catch (error) {
521
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.6",
4
+ "version": "0.3.8",
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.1-rc.2",
56
- "@deepseek-ai/dsh-launch-environment": "^0.1.1-rc.2",
57
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
58
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
59
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
60
- "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
61
- "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2",
62
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2",
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.1-rc.2",
71
- "@deepseek-ai/dsh-launch-environment": "^0.1.1-rc.2",
72
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
73
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
74
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
75
- "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
76
- "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2",
77
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2",
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
  }