@rayadesu/dsh-llm-billing 0.2.1 → 0.2.3

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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `billingTodaySpend` session-projection unit: per-session, per-Beijing-day
3
+ * billed spend, folded eagerly by the DSH projection drive over committed
4
+ * session events and checkpointed by the projection cache. The unit keeps only
5
+ * the spend of the session's LATEST priced day (events are append-only and
6
+ * chronological, so a day strictly older than the state's day never returns);
7
+ * the aggregate "today" read sums the units whose `dayKey` matches the current
8
+ * Beijing day — zero full-log scans once the fold is warm.
9
+ *
10
+ * The unit's fold shares {@link priceEvent} with the events-scan paths
11
+ * (`computeTodaySpend`), so both price with the same table. The unit is
12
+ * client-visible (`wire` = identity) because the persisted-cache read ladder
13
+ * (`sessionProjectionCache.coldSnapshot` / registry `restore`) serves only
14
+ * wired units; the wire value is the state itself.
15
+ * @module @rayadesu/dsh-llm-billing/projection
16
+ */
17
+ import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
18
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
19
+ import type { ResolvedBilling } from './billing.ts';
20
+ import type { DeepSeekTodaySpend } from './types.ts';
21
+ /** The projection key this unit owns. */
22
+ export declare const BILLING_UNIT_KEY = "billingTodaySpend";
23
+ /**
24
+ * Per-session unit state: the Beijing day of the session's latest priced
25
+ * event and that day's billed spend. `dayKey` is `''` while the session has no
26
+ * priced usage, and the state only ever describes ONE day (the latest) —
27
+ * plain JSON, as the persisted-cache contract requires.
28
+ */
29
+ export interface BillingUnitState {
30
+ /** Beijing-time calendar-day key of the state's spend; `''` for no priced usage. */
31
+ dayKey: string;
32
+ /** The spend of the session's latest priced Beijing day. */
33
+ spend: DeepSeekTodaySpend;
34
+ }
35
+ declare module '@deepseek-ai/dsh-session-projection/types' {
36
+ interface SessionProjectionStateMap {
37
+ billingTodaySpend: BillingUnitState;
38
+ }
39
+ interface SessionProjectionMap {
40
+ billingTodaySpend: BillingUnitState;
41
+ }
42
+ }
43
+ /**
44
+ * The unit definition with a required `wire` — the shape {@link register}
45
+ * accepts for a client-visible unit (the plain `ProjectionDefinition` type
46
+ * leaves `wire` optional).
47
+ */
48
+ export type BillingUnitDefinition = Omit<ProjectionDefinition<'billingTodaySpend', BillingUnitState>, 'wire'> & {
49
+ wire: NonNullable<ProjectionDefinition<'billingTodaySpend', BillingUnitState>['wire']>;
50
+ };
51
+ /**
52
+ * Build the `billingTodaySpend` unit for one resolved pricing table. The
53
+ * pricing closure is fixed at registration; a pricing-table change therefore
54
+ * prices only events folded after the change (historical spend keeps its
55
+ * historical rates), unlike the events-scan paths which re-price the whole
56
+ * log. Bump {@link ProjectionDefinition.stateVersion} whenever the state
57
+ * shape or fold semantics change, so persisted checkpoint rows are discarded
58
+ * instead of folded forward.
59
+ * @param billing - resolved pricing with peak-hour windows.
60
+ * @param catalog - model display rows, in presentation order.
61
+ * @returns the unit definition to register on `ctx.sessionProjections`.
62
+ */
63
+ export declare function billingTodaySpendDefinition(billing: ResolvedBilling, catalog: readonly {
64
+ id: string;
65
+ name: string;
66
+ }[]): BillingUnitDefinition;
67
+ /** 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;
69
+ //# sourceMappingURL=projection.d.ts.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * `billingTodaySpend` session-projection unit: per-session, per-Beijing-day
3
+ * billed spend, folded eagerly by the DSH projection drive over committed
4
+ * session events and checkpointed by the projection cache. The unit keeps only
5
+ * the spend of the session's LATEST priced day (events are append-only and
6
+ * chronological, so a day strictly older than the state's day never returns);
7
+ * the aggregate "today" read sums the units whose `dayKey` matches the current
8
+ * Beijing day — zero full-log scans once the fold is warm.
9
+ *
10
+ * The unit's fold shares {@link priceEvent} with the events-scan paths
11
+ * (`computeTodaySpend`), so both price with the same table. The unit is
12
+ * client-visible (`wire` = identity) because the persisted-cache read ladder
13
+ * (`sessionProjectionCache.coldSnapshot` / registry `restore`) serves only
14
+ * wired units; the wire value is the state itself.
15
+ * @module @rayadesu/dsh-llm-billing/projection
16
+ */
17
+ import { z } from 'zod';
18
+ import { addEventContribution, emptyTodaySpend, priceEvent } from "./billing.js";
19
+ /** The projection key this unit owns. */
20
+ export const BILLING_UNIT_KEY = 'billingTodaySpend';
21
+ const modelRowSchema = z.object({
22
+ model: z.string(),
23
+ displayName: z.string(),
24
+ cost: z.number().nonnegative(),
25
+ peakCost: z.number().nonnegative(),
26
+ offPeakCost: z.number().nonnegative(),
27
+ cacheHitInputTokens: z.number().int().nonnegative(),
28
+ cacheMissInputTokens: z.number().int().nonnegative(),
29
+ outputTokens: z.number().int().nonnegative(),
30
+ cacheHitInputCost: z.number().nonnegative(),
31
+ cacheMissInputCost: z.number().nonnegative(),
32
+ outputCost: z.number().nonnegative(),
33
+ }).strict();
34
+ const todaySpendSchema = z.object({
35
+ total: z.number().nonnegative(),
36
+ models: z.array(modelRowSchema),
37
+ }).strict();
38
+ const billingUnitSchema = z.object({
39
+ dayKey: z.string(),
40
+ spend: todaySpendSchema,
41
+ }).strict();
42
+ /**
43
+ * Build the `billingTodaySpend` unit for one resolved pricing table. The
44
+ * pricing closure is fixed at registration; a pricing-table change therefore
45
+ * prices only events folded after the change (historical spend keeps its
46
+ * historical rates), unlike the events-scan paths which re-price the whole
47
+ * log. Bump {@link ProjectionDefinition.stateVersion} whenever the state
48
+ * shape or fold semantics change, so persisted checkpoint rows are discarded
49
+ * instead of folded forward.
50
+ * @param billing - resolved pricing with peak-hour windows.
51
+ * @param catalog - model display rows, in presentation order.
52
+ * @returns the unit definition to register on `ctx.sessionProjections`.
53
+ */
54
+ export function billingTodaySpendDefinition(billing, catalog) {
55
+ const names = new Map(catalog.map(model => [model.id, model.name]));
56
+ return {
57
+ key: BILLING_UNIT_KEY,
58
+ stateVersion: 1,
59
+ stateSchema: billingUnitSchema,
60
+ init: () => ({ dayKey: '', spend: emptyTodaySpend() }),
61
+ apply: (state, event) => {
62
+ const priced = priceEvent(event, billing, names);
63
+ if (priced === undefined)
64
+ return state;
65
+ if (state.dayKey === priced.dayKey) {
66
+ return { dayKey: state.dayKey, spend: addEventContribution(state.spend, priced) };
67
+ }
68
+ // The session log is append-only and chronological, so an event whose
69
+ // Beijing day is strictly older than the state's day cannot legally
70
+ // follow it; ignore defensively to keep the persisted fold
71
+ // deterministic under reordered or clock-skewed timestamps.
72
+ if (state.dayKey !== '' && priced.dayKey < state.dayKey)
73
+ return state;
74
+ // First priced event, or the session's first priced event of a new day:
75
+ // the state resets to that day's spend.
76
+ return { dayKey: priced.dayKey, spend: addEventContribution(emptyTodaySpend(), priced) };
77
+ },
78
+ wire: { viewSchema: billingUnitSchema, view: state => state },
79
+ };
80
+ }
81
+ /** Fold a unit from init over one session's event log (the detached cold recipe). */
82
+ export function foldBillingUnit(unit, events) {
83
+ let state = unit.init();
84
+ for (const event of events)
85
+ state = unit.apply(state, event);
86
+ return state;
87
+ }
88
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Today-spend read path: the 60-second Beijing-day cache with in-flight
3
+ * coalescing and a force bypass (plan A1), plus the two scan strategies that
4
+ * compute the aggregate behind a cache miss:
5
+ *
6
+ * - projection path (plan C): live sessions read their eagerly folded
7
+ * `billingTodaySpend` projection cell; cold sessions resolve through the
8
+ * projection-cache ladder (cached row + tail replay + registry restore,
9
+ * with write-back) or, without the cache service, one detached local fold
10
+ * over a full `inspect`. Persisted revisions gate every cold read, so a
11
+ * session whose log did not change since the last resolution costs nothing.
12
+ * - events path (plans A2/A3): collect and price only today's events in one
13
+ * pass (per-event Beijing-day filter during collection) with a hard cap,
14
+ * skipping sessions whose persisted revision is unchanged since the last
15
+ * scan.
16
+ *
17
+ * Both strategies run behind the same {@link TodaySpendCache}, so a miss
18
+ * happens at most once per 60 seconds per process, and a manual refresh
19
+ * (`force`) bypasses the time window but keeps the revision caches — an
20
+ * unchanged log provably cannot change the aggregate.
21
+ * @module @rayadesu/dsh-llm-billing/today-spend
22
+ */
23
+ import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session';
24
+ import type { SessionPersistenceRevision } from '@deepseek-ai/dsh-session-persistence';
25
+ import type { ResolvedBilling } from './billing.ts';
26
+ import type { DeepSeekTodaySpend } from './types.ts';
27
+ import { BILLING_UNIT_KEY, type BillingUnitState } from './projection.ts';
28
+ import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
29
+ /** Structural slice of a live session the scanner reads. */
30
+ export interface ScannerSession {
31
+ readonly id: SessionId;
32
+ readonly events: readonly SessionEvent[];
33
+ }
34
+ /** Structural slice of a listed persisted session. */
35
+ export interface ScannerPersistedHeader {
36
+ readonly id: SessionId;
37
+ }
38
+ /** Structural slices of the optional services the scanner reads through. */
39
+ export interface TodaySpendScannerDeps {
40
+ /** Resolves the live SessionStore at scan time (absent in headless assemblies). */
41
+ sessions?: () => {
42
+ list(): readonly ScannerSession[];
43
+ } | undefined;
44
+ /** Resolves the persistence backend at scan time (absent without persistence). */
45
+ persistence?: () => {
46
+ listSnapshots(): Promise<readonly {
47
+ header: ScannerPersistedHeader;
48
+ revision: SessionPersistenceRevision;
49
+ }[]>;
50
+ inspect(id: SessionId): Promise<{
51
+ events: readonly SessionEvent[];
52
+ }>;
53
+ } | undefined;
54
+ /** Resolves the session-projection registry at scan time (absent → events path). */
55
+ projections?: () => {
56
+ stateOf(session: ScannerSession, key: typeof BILLING_UNIT_KEY): BillingUnitState | undefined;
57
+ } | undefined;
58
+ /** Resolves the projection cache at scan time (absent → detached fold for cold sessions). */
59
+ projectionCache?: () => {
60
+ coldSnapshot(id: SessionId): Promise<{
61
+ values: Partial<Record<typeof BILLING_UNIT_KEY, BillingUnitState>>;
62
+ }>;
63
+ } | undefined;
64
+ /**
65
+ * Registers the billing unit on the projection registry, called once before
66
+ * the first projection-path scan. The registry builds cells lazily over the
67
+ * in-memory log, so events committed before registration are folded on
68
+ * first touch — late registration is safe by design.
69
+ */
70
+ ensureUnit?: () => void;
71
+ /** The billing unit's fold (the projection path's detached cold recipe). */
72
+ unit: Pick<ProjectionDefinition<'billingTodaySpend', BillingUnitState>, 'init' | 'apply'>;
73
+ /** Hard cap on today's events collected by the events path. */
74
+ maxEvents: number;
75
+ /** Warn sink for truncation and unreadable sessions. */
76
+ logger: {
77
+ warn(message: string): void;
78
+ };
79
+ /** Pricing table resolved from the plugin config. */
80
+ billing: ResolvedBilling;
81
+ /** Model display rows, in presentation order. */
82
+ catalog: readonly {
83
+ id: string;
84
+ name: string;
85
+ }[];
86
+ }
87
+ /**
88
+ * The A1 cache: one Beijing-day key + a 60s window, an in-flight promise that
89
+ * coalesces concurrent misses, and a `force` bypass for the manual refresh
90
+ * path. Cross-day invalidation is automatic (the day key changes); a failed
91
+ * scan leaves the previous value in place and retries on the next call.
92
+ */
93
+ export declare class TodaySpendCache {
94
+ private readonly scan;
95
+ private readonly ttlMs;
96
+ private readonly now;
97
+ private cachedDayKey;
98
+ private cachedValue;
99
+ private cachedAt;
100
+ private inFlight;
101
+ /**
102
+ * @param ttlMs - time window in milliseconds (default 60 000).
103
+ * @param now - clock source (injectable for tests).
104
+ * @param scan - the aggregate computation behind a miss.
105
+ */
106
+ constructor(scan: (dayKey: string) => Promise<DeepSeekTodaySpend>, ttlMs?: number, now?: () => Date);
107
+ /**
108
+ * Read today's spend, cached per Beijing day within the TTL window.
109
+ * @param force - bypass the time window (manual refresh); the day-key gate
110
+ * and the in-flight coalescing still apply to non-force callers.
111
+ * @returns today's spend.
112
+ */
113
+ get(force?: boolean): Promise<DeepSeekTodaySpend>;
114
+ }
115
+ /**
116
+ * The aggregate computation behind a cache miss. Chooses the projection path
117
+ * when the projection registry is composed, the events path otherwise; both
118
+ * gate cold reads on persisted revisions so steady-state scans touch only
119
+ * sessions whose logs actually changed.
120
+ */
121
+ export declare class TodaySpendScanner {
122
+ private readonly deps;
123
+ /** Cold sessions resolved on the projection path: id → revision + unit state. */
124
+ private readonly coldResolved;
125
+ /** Cold sessions resolved on the events path: id → revision (events were collected). */
126
+ private lastEventsScan;
127
+ constructor(deps: TodaySpendScannerDeps);
128
+ /**
129
+ * Compute today's aggregate for one Beijing day.
130
+ * @param dayKey - the Beijing-time calendar-day key to aggregate.
131
+ * @returns today's spend across every session.
132
+ */
133
+ scan(dayKey: string): Promise<DeepSeekTodaySpend>;
134
+ /** Projection path: eager cells for live sessions, revision-gated cold ladder for the rest. */
135
+ private scanProjections;
136
+ /**
137
+ * Events path: price today's events in a single pass (per-event Beijing-day
138
+ * filter during collection, hard cap), gated by revisions.
139
+ */
140
+ private scanEvents;
141
+ }
142
+ //# sourceMappingURL=today-spend.d.ts.map
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Today-spend read path: the 60-second Beijing-day cache with in-flight
3
+ * coalescing and a force bypass (plan A1), plus the two scan strategies that
4
+ * compute the aggregate behind a cache miss:
5
+ *
6
+ * - projection path (plan C): live sessions read their eagerly folded
7
+ * `billingTodaySpend` projection cell; cold sessions resolve through the
8
+ * projection-cache ladder (cached row + tail replay + registry restore,
9
+ * with write-back) or, without the cache service, one detached local fold
10
+ * over a full `inspect`. Persisted revisions gate every cold read, so a
11
+ * session whose log did not change since the last resolution costs nothing.
12
+ * - events path (plans A2/A3): collect and price only today's events in one
13
+ * pass (per-event Beijing-day filter during collection) with a hard cap,
14
+ * skipping sessions whose persisted revision is unchanged since the last
15
+ * scan.
16
+ *
17
+ * Both strategies run behind the same {@link TodaySpendCache}, so a miss
18
+ * happens at most once per 60 seconds per process, and a manual refresh
19
+ * (`force`) bypasses the time window but keeps the revision caches — an
20
+ * unchanged log provably cannot change the aggregate.
21
+ * @module @rayadesu/dsh-llm-billing/today-spend
22
+ */
23
+ import { beijingDayKey, emptyTodaySpend, mergeTodaySpend, priceEvent, SpendAccumulator } from "./billing.js";
24
+ import { BILLING_UNIT_KEY, foldBillingUnit } from "./projection.js";
25
+ /**
26
+ * Bounded parallel fan-out: run `run` over `items` with at most `limit` in
27
+ * flight. A shared index counter hands each worker its next job, so the
28
+ * dispatch is O(n) overall (array `shift()` would be O(n) per pop).
29
+ */
30
+ async function withConcurrency(items, limit, run) {
31
+ const total = items.length;
32
+ let next = 0;
33
+ await Promise.all(Array.from({ length: Math.min(limit, total) }, async () => {
34
+ for (let job = next; job < total; job = next) {
35
+ next += 1;
36
+ await run(items[job]);
37
+ }
38
+ }));
39
+ }
40
+ /**
41
+ * The A1 cache: one Beijing-day key + a 60s window, an in-flight promise that
42
+ * coalesces concurrent misses, and a `force` bypass for the manual refresh
43
+ * path. Cross-day invalidation is automatic (the day key changes); a failed
44
+ * scan leaves the previous value in place and retries on the next call.
45
+ */
46
+ export class TodaySpendCache {
47
+ scan;
48
+ ttlMs;
49
+ now;
50
+ cachedDayKey;
51
+ cachedValue;
52
+ cachedAt = 0;
53
+ inFlight;
54
+ /**
55
+ * @param ttlMs - time window in milliseconds (default 60 000).
56
+ * @param now - clock source (injectable for tests).
57
+ * @param scan - the aggregate computation behind a miss.
58
+ */
59
+ constructor(scan, ttlMs = 60_000, now = () => new Date()) {
60
+ this.scan = scan;
61
+ this.ttlMs = ttlMs;
62
+ this.now = now;
63
+ }
64
+ /**
65
+ * Read today's spend, cached per Beijing day within the TTL window.
66
+ * @param force - bypass the time window (manual refresh); the day-key gate
67
+ * and the in-flight coalescing still apply to non-force callers.
68
+ * @returns today's spend.
69
+ */
70
+ get(force = false) {
71
+ const now = this.now();
72
+ const dayKey = beijingDayKey(now);
73
+ if (!force && this.cachedDayKey === dayKey && this.cachedValue !== undefined
74
+ && now.getTime() - this.cachedAt < this.ttlMs) {
75
+ return Promise.resolve(this.cachedValue);
76
+ }
77
+ if (!force && this.inFlight !== undefined)
78
+ return this.inFlight;
79
+ const run = (async () => {
80
+ try {
81
+ const value = await this.scan(dayKey);
82
+ this.cachedDayKey = dayKey;
83
+ this.cachedValue = value;
84
+ this.cachedAt = now.getTime();
85
+ return value;
86
+ }
87
+ finally {
88
+ this.inFlight = undefined;
89
+ }
90
+ })();
91
+ if (!force)
92
+ this.inFlight = run;
93
+ return run;
94
+ }
95
+ }
96
+ /**
97
+ * The aggregate computation behind a cache miss. Chooses the projection path
98
+ * when the projection registry is composed, the events path otherwise; both
99
+ * gate cold reads on persisted revisions so steady-state scans touch only
100
+ * sessions whose logs actually changed.
101
+ */
102
+ export class TodaySpendScanner {
103
+ deps;
104
+ /** Cold sessions resolved on the projection path: id → revision + unit state. */
105
+ coldResolved = new Map();
106
+ /** Cold sessions resolved on the events path: id → revision (events were collected). */
107
+ lastEventsScan;
108
+ constructor(deps) {
109
+ this.deps = deps;
110
+ }
111
+ /**
112
+ * Compute today's aggregate for one Beijing day.
113
+ * @param dayKey - the Beijing-time calendar-day key to aggregate.
114
+ * @returns today's spend across every session.
115
+ */
116
+ async scan(dayKey) {
117
+ if (this.deps.projections?.() === undefined)
118
+ return this.scanEvents(dayKey);
119
+ this.deps.ensureUnit?.();
120
+ return this.scanProjections(dayKey);
121
+ }
122
+ /** Projection path: eager cells for live sessions, revision-gated cold ladder for the rest. */
123
+ async scanProjections(dayKey) {
124
+ const { sessions, persistence, projections, projectionCache, unit, logger } = this.deps;
125
+ // Services resolve once per scan, not per session / per cold task.
126
+ const projectionsService = projections?.();
127
+ const cache = projectionCache?.();
128
+ let total = emptyTodaySpend();
129
+ const liveIds = new Set();
130
+ if (sessions !== undefined) {
131
+ const store = sessions();
132
+ if (store !== undefined) {
133
+ for (const session of store.list()) {
134
+ liveIds.add(session.id);
135
+ const state = projectionsService?.stateOf(session, BILLING_UNIT_KEY);
136
+ if (state !== undefined && state.dayKey === dayKey) {
137
+ total = mergeTodaySpend(total, state.spend);
138
+ }
139
+ }
140
+ }
141
+ }
142
+ const persistenceService = persistence?.();
143
+ if (persistenceService === undefined)
144
+ return total;
145
+ const snapshots = await persistenceService.listSnapshots();
146
+ const pending = [];
147
+ for (const { header, revision } of snapshots) {
148
+ if (liveIds.has(header.id))
149
+ continue;
150
+ const resolved = this.coldResolved.get(header.id);
151
+ if (resolved !== undefined && resolved.revision === revision) {
152
+ if (resolved.value.dayKey === dayKey)
153
+ total = mergeTodaySpend(total, resolved.value.spend);
154
+ continue;
155
+ }
156
+ pending.push({ id: header.id, revision });
157
+ }
158
+ await withConcurrency(pending, 8, async ({ id, revision }) => {
159
+ let value;
160
+ if (cache !== undefined) {
161
+ try {
162
+ value = (await cache.coldSnapshot(id)).values[BILLING_UNIT_KEY];
163
+ }
164
+ catch (error) {
165
+ logger.warn(`llm-billing: projection cold read for session ${id} failed: ${String(error)}`);
166
+ }
167
+ }
168
+ if (value === undefined) {
169
+ try {
170
+ const inspection = await persistenceService.inspect(id);
171
+ value = foldBillingUnit(unit, inspection.events);
172
+ }
173
+ catch (error) {
174
+ // One unreadable session must not blank the whole-day aggregate.
175
+ logger.warn(`llm-billing: skipping unreadable session ${id}: ${String(error)}`);
176
+ }
177
+ }
178
+ if (value !== undefined)
179
+ this.coldResolved.set(id, { revision, value });
180
+ });
181
+ for (const { id } of pending) {
182
+ const resolved = this.coldResolved.get(id);
183
+ if (resolved !== undefined && resolved.value.dayKey === dayKey) {
184
+ total = mergeTodaySpend(total, resolved.value.spend);
185
+ }
186
+ }
187
+ return total;
188
+ }
189
+ /**
190
+ * Events path: price today's events in a single pass (per-event Beijing-day
191
+ * filter during collection, hard cap), gated by revisions.
192
+ */
193
+ async scanEvents(dayKey) {
194
+ const { sessions, persistence, maxEvents, logger, billing, catalog } = this.deps;
195
+ const names = new Map(catalog.map(model => [model.id, model.name]));
196
+ const accumulator = new SpendAccumulator();
197
+ const liveIds = new Set();
198
+ let collected = 0;
199
+ let truncated = false;
200
+ const collect = (events) => {
201
+ for (const event of events) {
202
+ if (beijingDayKey(new Date(event.time)) !== dayKey)
203
+ continue;
204
+ collected += 1;
205
+ if (collected > maxEvents) {
206
+ truncated = true;
207
+ return;
208
+ }
209
+ const priced = priceEvent(event, billing, names);
210
+ if (priced !== undefined)
211
+ accumulator.add(priced);
212
+ }
213
+ };
214
+ if (sessions !== undefined) {
215
+ const store = sessions();
216
+ if (store !== undefined) {
217
+ for (const session of store.list()) {
218
+ liveIds.add(session.id);
219
+ collect(session.events);
220
+ if (truncated)
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ const persistenceService = persistence?.();
226
+ if (!truncated && persistenceService !== undefined) {
227
+ const snapshots = await persistenceService.listSnapshots();
228
+ for (const { header, revision } of snapshots) {
229
+ if (liveIds.has(header.id))
230
+ continue;
231
+ if (this.lastEventsScan?.get(header.id) === revision)
232
+ continue;
233
+ try {
234
+ collect((await persistenceService.inspect(header.id)).events);
235
+ }
236
+ catch (error) {
237
+ // One unreadable session must not blank the whole-day aggregate.
238
+ logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
239
+ }
240
+ if (truncated)
241
+ break;
242
+ }
243
+ // Only a complete pass may advance the revision watermark: a truncated
244
+ // pass left sessions unread, and recording them would skip their events
245
+ // on the next scan.
246
+ if (!truncated) {
247
+ this.lastEventsScan = new Map(snapshots.map(snapshot => [snapshot.header.id, snapshot.revision]));
248
+ }
249
+ }
250
+ if (truncated)
251
+ logger.warn(`llm-billing: today's events exceeded ${maxEvents}; result truncated`);
252
+ return accumulator.finish();
253
+ }
254
+ }
255
+ //# sourceMappingURL=today-spend.js.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Client-safe balance and spend vocabulary shared by the `billing` Remote,
3
3
  * its generated artifacts, and the web UI.
4
- * @module @deepseek-ai/dsh-llm-billing/types
4
+ * @module @rayadesu/dsh-llm-billing/types
5
5
  */
6
6
  /** One currency line of the account balance returned by `GET /user/balance`. */
7
7
  export interface DeepSeekBalanceLine {
@@ -46,14 +46,14 @@ export interface DeepSeekSessionSpendModel {
46
46
  /** Billed cost of output tokens (reasoning included) in CNY. */
47
47
  outputCost: number;
48
48
  }
49
- /** The billed spend of one session, priced per event by its Beijing-time peak/off-peak hour. */
49
+ /** The billed spend of one session, priced per event by its Beijing-time hour and weekday (peak hours apply Monday–Friday only; weekends are off-peak). */
50
50
  export interface DeepSeekSessionSpend {
51
51
  /** Total billed cost in CNY across every priced model. */
52
52
  total: number;
53
53
  /** One row per model that reported usage AND has a pricing row; empty when the session has no priced usage. */
54
54
  models: readonly DeepSeekSessionSpendModel[];
55
55
  }
56
- /** The billed spend of every session on one Beijing-time calendar day, priced per event by its peak/off-peak hour. */
56
+ /** The billed spend of every session on one Beijing-time calendar day, priced per event by its Beijing-time hour and weekday (peak hours apply Monday–Friday only; weekends are off-peak). */
57
57
  export interface DeepSeekTodaySpend {
58
58
  /** Total billed cost in CNY across every priced model and every session. */
59
59
  total: number;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Client-safe balance and spend vocabulary shared by the `billing` Remote,
3
3
  * its generated artifacts, and the web UI.
4
- * @module @deepseek-ai/dsh-llm-billing/types
4
+ * @module @rayadesu/dsh-llm-billing/types
5
5
  */
6
6
  export {};
7
7
  //# sourceMappingURL=types.js.map
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.2.1",
4
+ "version": "0.2.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -47,16 +47,19 @@
47
47
  "lib/typert.remote-client.js",
48
48
  "lib/typert.remote-client.d.ts"
49
49
  ],
50
+ "scripts": {
51
+ "prepublishOnly": "node ../../scripts/verify-packages.mjs"
52
+ },
50
53
  "license": "MIT",
51
54
  "peerDependencies": {
52
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
53
- "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8",
54
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
55
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
56
- "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
57
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8",
58
- "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8",
59
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8",
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",
60
63
  "@deepseek-ai/cordis": "^4.0.1"
61
64
  },
62
65
  "dependencies": {
@@ -64,14 +67,14 @@
64
67
  "zod": "^4.4.3"
65
68
  },
66
69
  "devDependencies": {
67
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
68
- "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.8",
69
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
70
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
71
- "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
72
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8",
73
- "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8",
74
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8",
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",
75
78
  "@deepseek-ai/cordis": "^4.0.1"
76
79
  }
77
80
  }