@rayadesu/dsh-llm-billing 0.3.15 → 0.3.17
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 +18 -7
- package/README.zh.md +19 -8
- package/icon.svg +7 -0
- package/lib/index.js +791 -423
- package/lib/typert.host.js +17 -7
- package/lib/typert.remote-client.js +17 -7
- package/lib/types/balance.d.ts +10 -0
- package/lib/types/balance.js +79 -13
- package/lib/types/beijing-time.d.ts +52 -0
- package/lib/types/beijing-time.js +88 -0
- package/lib/types/billing.d.ts +27 -388
- package/lib/types/billing.js +28 -614
- package/lib/types/cache.d.ts +110 -0
- package/lib/types/cache.js +155 -0
- package/lib/types/fold.d.ts +93 -0
- package/lib/types/fold.js +188 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.js +8 -5
- package/lib/types/persistence.d.ts +113 -0
- package/lib/types/persistence.js +63 -0
- package/lib/types/pricing-table.d.ts +43 -0
- package/lib/types/pricing-table.js +118 -0
- package/lib/types/pricing.d.ts +203 -0
- package/lib/types/pricing.js +203 -0
- package/lib/types/projection.js +14 -0
- package/lib/types/session-lineage.d.ts +70 -0
- package/lib/types/session-lineage.js +99 -0
- package/lib/types/spend.d.ts +58 -0
- package/lib/types/spend.js +190 -0
- package/lib/types/today-spend.d.ts +27 -233
- package/lib/types/today-spend.js +73 -254
- package/lib/types/types.d.ts +28 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +5 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `billingTodaySpend` projection cell; cold sessions are answered from the
|
|
8
8
|
* zero-I/O projection-cache row whenever that row's own day is not the
|
|
9
9
|
* queried one, and otherwise resolved through one detached local fold over
|
|
10
|
-
* a full `
|
|
10
|
+
* a full `open` + handle read. Persisted revisions gate every cold read, so a session
|
|
11
11
|
* whose log did not change since the last resolution costs nothing — and a
|
|
12
12
|
* failed resolution is remembered by revision instead of being retried on
|
|
13
13
|
* every scan.
|
|
@@ -38,208 +38,30 @@
|
|
|
38
38
|
* cut, and `apply` skips events below it), so the eager cell is correct for a
|
|
39
39
|
* fork child; the cold path skips the projection cache for a seeded session
|
|
40
40
|
* (its cached row may predate the boundary) and folds its own events with the
|
|
41
|
-
* durable cut instead. The boundary is the durable session state, read
|
|
42
|
-
*
|
|
41
|
+
* durable cut instead. The boundary is the durable session state, read from the
|
|
42
|
+
* live-session and handle surfaces — a resumed fork child keeps its original
|
|
43
43
|
* boundary and an unseeded session stays at 0.
|
|
44
44
|
*
|
|
45
45
|
* The live `Session` log surface changed in 0.1.2-alpha.4: `Session.events`
|
|
46
46
|
* was removed and replaced by `Session.snapshotEvents()` / `ownEvents()`, and
|
|
47
47
|
* `SessionHeader.seedLength` moved to `Session.inheritedEventCount` (the
|
|
48
|
-
*
|
|
49
|
-
* {@link liveSessionEvents} / {@link forkBoundaryOf}, which accept
|
|
50
|
-
*
|
|
51
|
-
* and on the newer runtime.
|
|
48
|
+
* opened `SessionHandle` carries it alongside `header`). Reads go through
|
|
49
|
+
* {@link liveSessionEvents} / {@link forkBoundaryOf}, which accept the
|
|
50
|
+
* live-session and handle shapes structurally, so the scanner runs on the
|
|
51
|
+
* older `events` runtime and on the newer `snapshotEvents` runtime.
|
|
52
52
|
* @module @rayadesu/dsh-llm-billing/today-spend
|
|
53
53
|
*/
|
|
54
|
-
import type {
|
|
54
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
55
55
|
import type { SessionPersistenceRevision } from '@deepseek-ai/dsh-session-persistence';
|
|
56
56
|
import type { ResolvedBilling } from './billing.ts';
|
|
57
57
|
import type { BillingFoldState } from './billing.ts';
|
|
58
58
|
import type { DeepSeekTodaySessionSpend, DeepSeekTodaySessionsSpend, DeepSeekTodaySpend } from './types.ts';
|
|
59
59
|
import { BILLING_UNIT_KEY, type BillingUnitFold, type BillingUnitState } from './projection.ts';
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* @param events - one session's complete event log.
|
|
66
|
-
* @returns the session's current title, or `null` when untitled.
|
|
67
|
-
*/
|
|
68
|
-
export declare function foldSessionTitle(events: readonly SessionEvent[]): string | null;
|
|
69
|
-
/**
|
|
70
|
-
* Structural slice of a session's durable lineage: the header fields DSH
|
|
71
|
-
* stamps on a delegation child (`origin: 'subagent'`, the delegating session's
|
|
72
|
-
* id, and the depth that survives persistence). All three are read
|
|
73
|
-
* structurally, so the scanner works on every runtime family — a log written
|
|
74
|
-
* before the fields existed simply carries none of them and reads as a
|
|
75
|
-
* top-level session.
|
|
76
|
-
*/
|
|
77
|
-
export interface SessionLineage {
|
|
78
|
-
/** The session this one was forked from or delegated by; absent for a top-level session. */
|
|
79
|
-
readonly parentSession?: SessionId;
|
|
80
|
-
/** DSH's subagent-child classification (`childSessionMeta` stamps it). */
|
|
81
|
-
readonly origin?: 'subagent';
|
|
82
|
-
/** Delegation depth: absent (zero) at the top level, parent depth + 1 for a subagent child. */
|
|
83
|
-
readonly delegationDepth?: number;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Whether one session's durable header marks it as a subagent child. Either
|
|
87
|
-
* marker is enough: `origin` is DSH's navigation classification and
|
|
88
|
-
* `delegationDepth` is its persisted recursion budget, so a header carrying
|
|
89
|
-
* only the depth (or only the origin) is still a delegation child. A session
|
|
90
|
-
* created without either — an ordinary session, a user fork, or a cold resume
|
|
91
|
-
* — is top-level.
|
|
92
|
-
* @param header - the session's lineage slice; `undefined` reads as top-level.
|
|
93
|
-
* @returns true when the session was created as a subagent child.
|
|
94
|
-
*/
|
|
95
|
-
export declare function isSubagentSession(header: SessionLineage | undefined): boolean;
|
|
96
|
-
/**
|
|
97
|
-
* The top-level session one session's ranking row belongs to: the session
|
|
98
|
-
* itself for a top-level session, and for a subagent child the first ancestor
|
|
99
|
-
* up the `parentSession` chain that is not itself a subagent child. A
|
|
100
|
-
* multi-generation delegation (a subagent that spawned subagents) therefore
|
|
101
|
-
* lands on the same root row as its parent, and a child whose parent header is
|
|
102
|
-
* unknown is attributed to the parent id its own header names — the parent is
|
|
103
|
-
* authoritative even when its log is not part of this scan.
|
|
104
|
-
* @param id - the session whose row is being attributed.
|
|
105
|
-
* @param lineage - lineage of every session this scan saw, by id.
|
|
106
|
-
* @returns the session id whose ranking row the input belongs to.
|
|
107
|
-
*/
|
|
108
|
-
export declare function topLevelSessionOf(id: SessionId, lineage: ReadonlyMap<SessionId, SessionLineage>): SessionId;
|
|
109
|
-
/**
|
|
110
|
-
* Fold every subagent child's row into the top-level row it belongs to
|
|
111
|
-
* ({@link topLevelSessionOf}), so the ranking lists conversations rather than
|
|
112
|
-
* every delegation a conversation started. A child's spend is added to its
|
|
113
|
-
* ancestor's `total`; the ancestor's `ownTotal` keeps its own spend only. A
|
|
114
|
-
* top-level session whose own day was empty but whose subagents priced
|
|
115
|
-
* something still gets a row (with `ownTotal` 0), carrying the title the scan
|
|
116
|
-
* resolved for it in `titles`.
|
|
117
|
-
* @param rows - one row per session that priced something today (own spends).
|
|
118
|
-
* @param lineage - lineage of every session this scan saw, by id.
|
|
119
|
-
* @param titles - resolved display titles by session id; a session absent from
|
|
120
|
-
* the map has no resolved title and its created row reports `null`.
|
|
121
|
-
* @returns the merged rows, sorted by `total` descending.
|
|
122
|
-
*/
|
|
123
|
-
export declare function rollUpSubagentSpend(rows: readonly DeepSeekTodaySessionSpend[], lineage: ReadonlyMap<SessionId, SessionLineage>, titles?: ReadonlyMap<SessionId, string | null>): DeepSeekTodaySessionSpend[];
|
|
124
|
-
/**
|
|
125
|
-
* Structural slice of a live session's header: the fork boundary of both DSH
|
|
126
|
-
* runtime families plus the delegation lineage the ranking roll-up reads.
|
|
127
|
-
*/
|
|
128
|
-
export interface SessionHeaderSlice extends SessionLineage {
|
|
129
|
-
/** ≤ 0.1.1-rc.2: the durable fork boundary carried by the header; absent for an unseeded session. */
|
|
130
|
-
readonly seedLength?: number;
|
|
131
|
-
/** 0.1.2-alpha.4+: whether the session has a fork-inherited prefix. */
|
|
132
|
-
readonly isSeeded?: boolean;
|
|
133
|
-
/**
|
|
134
|
-
* Durable creation instant (epoch ms). The conversation read uses it to tell
|
|
135
|
-
* whether a session's spend can span more than the queried day; it is also
|
|
136
|
-
* part of the projection-cache record identity.
|
|
137
|
-
*/
|
|
138
|
-
readonly createdAt?: number;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Structural slice of a live session the scanner reads, accepting both DSH
|
|
142
|
-
* runtime families: the ≤ 0.1.1-rc.2 baseline exposes the log as
|
|
143
|
-
* `events` (+ `header.seedLength`), while 0.1.2-alpha.4+ exposes
|
|
144
|
-
* `snapshotEvents()` and `inheritedEventCount` (and dropped the header field).
|
|
145
|
-
*/
|
|
146
|
-
export interface ScannerSession {
|
|
147
|
-
readonly id: SessionId;
|
|
148
|
-
/** ≤ 0.1.1-rc.2: the full event log snapshot. */
|
|
149
|
-
readonly events?: readonly SessionEvent[];
|
|
150
|
-
/** 0.1.2-alpha.4+: materialize an immutable log snapshot; no args = the full current log. */
|
|
151
|
-
snapshotEvents?(fromSeq?: number, toSeqExclusive?: number): readonly SessionEvent[];
|
|
152
|
-
/** 0.1.2-alpha.4+: the durable inherited-prefix length (0 for an unseeded session). */
|
|
153
|
-
readonly inheritedEventCount?: number;
|
|
154
|
-
/** Durable header slice: `seedLength` (older runtime) or `isSeeded` (newer runtime), plus lineage. */
|
|
155
|
-
readonly header?: SessionHeaderSlice;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Read one live session's complete event log across both runtime families.
|
|
159
|
-
* @throws when the session exposes neither the legacy `events` snapshot nor
|
|
160
|
-
* the newer `snapshotEvents()` reader — an unknown runtime surface must
|
|
161
|
-
* fail loudly rather than silently price an empty log.
|
|
162
|
-
*/
|
|
163
|
-
export declare function liveSessionEvents(session: ScannerSession): readonly SessionEvent[];
|
|
164
|
-
/** Structural slice of a listed persisted session (the snapshot header is a full SessionHeader). */
|
|
165
|
-
export interface ScannerPersistedHeader extends SessionHeaderSlice {
|
|
166
|
-
readonly id: SessionId;
|
|
167
|
-
/** Session format generation; part of the projection-cache record identity. */
|
|
168
|
-
readonly version?: number;
|
|
169
|
-
/** Working directory recorded on the header; part of the projection-cache record identity. */
|
|
170
|
-
readonly cwd?: string;
|
|
171
|
-
}
|
|
172
|
-
/** One stored-session read: the full event log plus the durable inherited boundary. */
|
|
173
|
-
export interface ScannerPersistedRead {
|
|
174
|
-
readonly events: readonly SessionEvent[];
|
|
175
|
-
/** Inherited-prefix length (fork seed length); 0 for an unseeded session. */
|
|
176
|
-
readonly seedLength: number;
|
|
177
|
-
}
|
|
178
|
-
/** ≤ 0.1.1-rc.2 persistence slice: service-level `inspect` / `listSnapshots`. */
|
|
179
|
-
export interface ScannerPersistenceLegacy {
|
|
180
|
-
listSnapshots(): Promise<readonly {
|
|
181
|
-
header: ScannerPersistedHeader;
|
|
182
|
-
revision: SessionPersistenceRevision;
|
|
183
|
-
}[]>;
|
|
184
|
-
inspect(id: SessionId): Promise<{
|
|
185
|
-
meta?: SessionHeaderSlice;
|
|
186
|
-
/** 0.1.2-alpha.4+: the exact inherited cut travels beside, not inside, the header. */
|
|
187
|
-
inheritedEventCount?: number;
|
|
188
|
-
events: readonly SessionEvent[];
|
|
189
|
-
}>;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* One handle read result across DSH generations. The handle seam first
|
|
193
|
-
* returned the bare event array; since `9b78f99dec` (2026-09-06, in the
|
|
194
|
-
* 0.1.5-alpha.1 checkout) it returns `{ eventState, events }`. Both shapes are
|
|
195
|
-
* accepted so the same build serves the npm alpha line and the checkout.
|
|
196
|
-
*/
|
|
197
|
-
export type ScannerHandleRead = readonly SessionEvent[] | {
|
|
198
|
-
readonly events: readonly SessionEvent[];
|
|
199
|
-
};
|
|
200
|
-
/**
|
|
201
|
-
* Unwrap a handle read across both return shapes.
|
|
202
|
-
* @param read - the handle's read result.
|
|
203
|
-
* @returns the event array.
|
|
204
|
-
*/
|
|
205
|
-
export declare function handleReadEvents(read: ScannerHandleRead): readonly SessionEvent[];
|
|
206
|
-
/** 0.1.2-alpha.5+ handle-based persistence slice: service-level `list` / `open` + `SessionHandle`. */
|
|
207
|
-
export interface ScannerPersistenceHandle {
|
|
208
|
-
list(): Promise<readonly {
|
|
209
|
-
header: ScannerPersistedHeader;
|
|
210
|
-
revision: SessionPersistenceRevision;
|
|
211
|
-
}[]>;
|
|
212
|
-
open(id: SessionId, access: 'read'): Promise<{
|
|
213
|
-
readonly header?: SessionHeaderSlice;
|
|
214
|
-
/** 0.1.2-alpha.5+: the handle carries the exact inherited cut beside the header. */
|
|
215
|
-
readonly inheritedEventCount?: number;
|
|
216
|
-
read(): Promise<ScannerHandleRead>;
|
|
217
|
-
close(): Promise<void>;
|
|
218
|
-
}>;
|
|
219
|
-
}
|
|
220
|
-
/** Structural union the scanner reads through, accepting both persistence runtime families. */
|
|
221
|
-
export type ScannerPersistence = ScannerPersistenceLegacy | ScannerPersistenceHandle;
|
|
222
|
-
/**
|
|
223
|
-
* List every stored session snapshot across both persistence runtime families:
|
|
224
|
-
* `listSnapshots` (≤ 0.1.1-rc.2) or `list` (0.1.2-alpha.5+).
|
|
225
|
-
* @param persistence - the persistence service slice.
|
|
226
|
-
* @returns one snapshot per stored session.
|
|
227
|
-
*/
|
|
228
|
-
export declare function persistenceListSnapshots(persistence: ScannerPersistence): Promise<readonly {
|
|
229
|
-
header: ScannerPersistedHeader;
|
|
230
|
-
revision: SessionPersistenceRevision;
|
|
231
|
-
}[]>;
|
|
232
|
-
/**
|
|
233
|
-
* Read one stored session's complete event log and durable inherited boundary
|
|
234
|
-
* across both persistence runtime families: legacy `inspect` (≤ 0.1.1-rc.2)
|
|
235
|
-
* or `open` + handle `read` (0.1.2-alpha.5+; the handle is closed after the
|
|
236
|
-
* read). Both throw when the session does not exist.
|
|
237
|
-
* @param persistence - the persistence service slice.
|
|
238
|
-
* @param id - the stored session to read.
|
|
239
|
-
* @returns the session's complete event log plus its inherited-prefix boundary.
|
|
240
|
-
*/
|
|
241
|
-
export declare function persistenceInspect(persistence: ScannerPersistence, id: SessionId): Promise<ScannerPersistedRead>;
|
|
242
|
-
/** Structural slices of the optional services the scanner reads through. */
|
|
60
|
+
import { type SessionLineage } from './session-lineage.ts';
|
|
61
|
+
import type { ScannerPersistedHeader, ScannerPersistence, ScannerSession } from './persistence.ts';
|
|
62
|
+
export * from './session-lineage.ts';
|
|
63
|
+
export * from './persistence.ts';
|
|
64
|
+
export * from './cache.ts';
|
|
243
65
|
export interface TodaySpendScannerDeps {
|
|
244
66
|
/** Resolves the live SessionStore at scan time (absent in headless assemblies). */
|
|
245
67
|
sessions?: () => {
|
|
@@ -285,42 +107,6 @@ export interface TodaySpendScannerDeps {
|
|
|
285
107
|
name: string;
|
|
286
108
|
}[];
|
|
287
109
|
}
|
|
288
|
-
/**
|
|
289
|
-
* The A1 cache: one Beijing-day key + a 60s window, an in-flight promise that
|
|
290
|
-
* coalesces concurrent misses, and a `force` bypass for the manual refresh
|
|
291
|
-
* path. Cross-day invalidation is automatic (the day key changes); a failed
|
|
292
|
-
* scan leaves the previous value in place and retries on the next call.
|
|
293
|
-
* @typeParam T - the cached aggregate's value shape (the spend or its
|
|
294
|
-
* per-session breakdown).
|
|
295
|
-
*/
|
|
296
|
-
export declare class TodaySpendCache<T = DeepSeekTodaySpend> {
|
|
297
|
-
private readonly scan;
|
|
298
|
-
private readonly ttlMs;
|
|
299
|
-
private readonly now;
|
|
300
|
-
private cachedDayKey;
|
|
301
|
-
private cachedValue;
|
|
302
|
-
private cachedAt;
|
|
303
|
-
private inFlight;
|
|
304
|
-
/**
|
|
305
|
-
* @param scan - the aggregate computation behind a miss.
|
|
306
|
-
* @param ttlMs - time window in milliseconds (default 60 000).
|
|
307
|
-
* @param now - clock source (injectable for tests).
|
|
308
|
-
*/
|
|
309
|
-
constructor(scan: (dayKey: string) => Promise<T>, ttlMs?: number, now?: () => Date);
|
|
310
|
-
/**
|
|
311
|
-
* Read today's spend, cached per Beijing day within the TTL window.
|
|
312
|
-
* @param force - bypass the time window (manual refresh); the day-key gate
|
|
313
|
-
* and the in-flight coalescing still apply to non-force callers.
|
|
314
|
-
* @returns today's spend.
|
|
315
|
-
*/
|
|
316
|
-
get(force?: boolean): Promise<T>;
|
|
317
|
-
}
|
|
318
|
-
/** Max session-ids kept in the scanner's cold-resolution cache before eviction. */
|
|
319
|
-
export declare const COLD_RESOLVE_CACHE_LIMIT = 1024;
|
|
320
|
-
/** Max session-ids kept in the scanner's cold-failure cache before eviction. */
|
|
321
|
-
export declare const COLD_FAILED_CACHE_LIMIT = 1024;
|
|
322
|
-
/** Bounded parallel fan-out for cold-session resolution. */
|
|
323
|
-
export declare const COLD_RESOLVE_CONCURRENCY = 8;
|
|
324
110
|
/**
|
|
325
111
|
* One day's aggregate plus its per-session ranking, computed in a single pass:
|
|
326
112
|
* the aggregate is the sum of the rows, so both reads share every session
|
|
@@ -402,7 +188,12 @@ export declare class TodaySpendScanner {
|
|
|
402
188
|
* from the aggregate and the ranking on every scan after the first.
|
|
403
189
|
*/
|
|
404
190
|
private readonly coldResolved;
|
|
405
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Cold sessions whose resolution failed, id → the revision it failed at plus
|
|
193
|
+
* when. Keyed by revision too, so an unchanged log is not re-read every scan,
|
|
194
|
+
* but the retry is TIME-boxed: a purely revision-keyed failure would hide a
|
|
195
|
+
* session permanently when its log never changes again.
|
|
196
|
+
*/
|
|
406
197
|
private readonly coldFailed;
|
|
407
198
|
constructor(deps: TodaySpendScannerDeps);
|
|
408
199
|
/**
|
|
@@ -436,7 +227,10 @@ export declare class TodaySpendScanner {
|
|
|
436
227
|
* otherwise.
|
|
437
228
|
*
|
|
438
229
|
* The aggregate sums every priced session, subagents included — the ranking's
|
|
439
|
-
* subagent roll-up only regroups rows, so neither total moves.
|
|
230
|
+
* subagent roll-up only regroups rows, so neither total moves. Both paths
|
|
231
|
+
* fold in slices and hand the host's event loop back every
|
|
232
|
+
* {@link SCAN_YIELD_SESSIONS} sessions, so a long scan never starves the
|
|
233
|
+
* GUI's own round trips.
|
|
440
234
|
* @param dayKey - the Beijing-time calendar-day key to aggregate.
|
|
441
235
|
* @returns the aggregate plus per-session rows sorted by cost descending.
|
|
442
236
|
*/
|
|
@@ -448,14 +242,14 @@ export declare class TodaySpendScanner {
|
|
|
448
242
|
* own latest priced day is NOT the queried day: the row then proves the
|
|
449
243
|
* session contributed nothing to the queried day, so the log is never read.
|
|
450
244
|
* When the row IS the queried day (or no usable row exists) the session is
|
|
451
|
-
*
|
|
245
|
+
* opened and folded locally, because the row may trail the log (a crash
|
|
452
246
|
* between the last checkpoint and the session's last event).
|
|
453
247
|
*
|
|
454
248
|
* A cache-served value carries no title (the ladder only stores projection
|
|
455
249
|
* values), so such rows report `title: null`. A SEEDED session (fork child)
|
|
456
250
|
* skips the cache entirely: its cached row was folded over the inherited
|
|
457
|
-
* prefix too, so it always detaches through
|
|
458
|
-
* boundary (the
|
|
251
|
+
* prefix too, so it always detaches through a handle read with the durable
|
|
252
|
+
* boundary (the handle's `inheritedEventCount` or `header.seedLength`,
|
|
459
253
|
* depending on the runtime family) applied to the local fold.
|
|
460
254
|
* @param header - the listed session header (the cache identity witness).
|
|
461
255
|
* @param seeded - whether the session carries a fork-inherited prefix.
|
|
@@ -504,7 +298,7 @@ export declare class TodaySpendScanner {
|
|
|
504
298
|
/**
|
|
505
299
|
* Projection path, one pass for both outputs: eager cells for live sessions
|
|
506
300
|
* (title folded from the live log, so a rename is reflected immediately),
|
|
507
|
-
* revision-gated cold ladder for the rest (title resolved on
|
|
301
|
+
* revision-gated cold ladder for the rest (title resolved on the handle read, `null`
|
|
508
302
|
* when answered from the projection cache). A fork child's cell covers its
|
|
509
303
|
* inherited prefix, so its own-events fold supplies both outputs. Lineage
|
|
510
304
|
* (which session delegated which) comes from the same headers the boundary
|