@rayadesu/dsh-llm-billing 0.3.16 → 0.3.18

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.
@@ -1,198 +1,42 @@
1
1
  /**
2
- * DeepSeek billing: the peak/off-peak pricing table and the per-session spend
3
- * pricing. Pure functions over session events and the pricing table, so the
4
- * Remote gateway stays transport-free and the whole spend is testable without
5
- * a key.
2
+ * DeepSeek billing aggregation: the session/turn/today spend computations
3
+ * over the pricing engine (`pricing.ts`) and the per-event fold state machine
4
+ * (`fold.ts`). Pure functions over session events, so the whole spend is
5
+ * testable without a key and the Remote gateway stays transport-free.
6
6
  *
7
- * The per-event pricing lives in {@link priceEvent} and the fold in the
8
- * {@link SpendAccumulator}: the events-scan paths ({@link computeSessionSpend},
9
- * {@link computeTodaySpend}), the session-projection unit (`billingTodaySpend`
10
- * in projection.ts), and the scanner's single-pass events path all price
11
- * through the same primitives, so a pricing-table change cannot drift one path
12
- * from the others.
7
+ * The fold in {@link BillingFolder} / {@link applyBillingEvent} (see `fold.ts`)
8
+ * and the per-event pricing in `pricing.ts` are the single source of truth the
9
+ * events-scan paths, the session-projection unit, and the scanner all price
10
+ * through, so a pricing-table change cannot drift one path from the others.
13
11
  * @module @rayadesu/dsh-llm-billing/billing
14
12
  */
15
- import type { TokenUsage } from '@deepseek-ai/dsh-llm';
16
13
  import type { SessionEvent } from '@deepseek-ai/dsh-session';
17
14
  import type { DeepSeekSessionSpend, DeepSeekSessionTurnSpends, DeepSeekTodaySpend, DeepSeekTurnSpend } from './types.ts';
18
- /** One token price point, in CNY per 1M tokens. */
19
- export interface DeepSeekTokenPrice {
20
- /** 1M input cache-hit tokens. */
21
- cacheHitInput: number;
22
- /** 1M input cache-miss tokens (cache writes bill at this rate too). */
23
- cacheMissInput: number;
24
- /** 1M output tokens. */
25
- output: number;
26
- }
27
- /**
28
- * One published peak/off-peak rate revision of a model: the price pair plus the
29
- * instant it took effect. A provider re-prices a series without repricing its
30
- * history, so the table keeps every revision and prices each sample at the
31
- * rates of the sample's own timestamp.
32
- */
33
- export interface DeepSeekRateRevision {
34
- /** Price during peak hours. */
35
- peak: DeepSeekTokenPrice;
36
- /** Price during off-peak hours. */
37
- offPeak: DeepSeekTokenPrice;
38
- /**
39
- * Inclusive epoch ms this revision takes effect: it prices every sample at or
40
- * after that instant. `undefined` on a model's base revision, which also
41
- * covers every earlier instant.
42
- */
43
- effectiveFrom?: number;
44
- }
45
- /** Resolved pricing for one model: its published rate revisions, oldest first. */
46
- export interface DeepSeekModelPricing {
47
- /** Peak-hour price of the newest revision (the rates in effect now). */
48
- peak: DeepSeekTokenPrice;
49
- /** Off-peak price of the newest revision (the rates in effect now). */
50
- offPeak: DeepSeekTokenPrice;
51
- /**
52
- * Every published revision of this model, ascending by `effectiveFrom` (an
53
- * undated base revision first); always at least one. {@link priceUsage} picks
54
- * the revision in effect at the priced sample's own timestamp.
55
- */
56
- revisions: readonly DeepSeekRateRevision[];
57
- }
58
- /** One model's pricing-table row in configuration form. */
59
- export interface BillingConfigModel extends DeepSeekRateRevision {
60
- /** Wire model id. */
61
- model: string;
62
- }
63
- /** One peak-hour window on a 24h Beijing-time clock, applied weekdays only. */
64
- export interface PeakHourWindow {
65
- /** Inclusive start hour, `0`–`23`. */
66
- start: number;
67
- /** Exclusive end hour, `1`–`24`. */
68
- end: number;
69
- }
70
- /** Optional billing configuration; omission uses the published defaults. */
71
- export interface BillingConfig {
72
- /**
73
- * Peak-hour windows in Beijing time, applied on weekdays (Monday–Friday)
74
- * only; weekends (Saturday and Sunday) are always off-peak.
75
- */
76
- peakHours?: PeakHourWindow[];
77
- /**
78
- * Per-model pricing rows; omission uses the published V4 and MiMo rates.
79
- * Several rows for one model declare that model's rate history, priced per
80
- * sample by `effectiveFrom` (see {@link BillingConfigModel}).
81
- */
82
- models?: BillingConfigModel[];
83
- }
84
- /**
85
- * Published peak-hour windows (Beijing time): 09:00–12:00 and 14:00–18:00,
86
- * applied on weekdays (Monday–Friday) only — weekends are always off-peak
87
- * (effective 2026-08-23).
88
- */
89
- export declare const DEFAULT_PEAK_HOURS: readonly PeakHourWindow[];
90
- /**
91
- * Inclusive epoch ms of the published V4 Flash series re-pricing:
92
- * 2026-09-10 12:00 Beijing time (UTC+8, no DST) = 04:00 UTC. Samples before
93
- * this instant keep the base rates; samples at or after it bill at the second
94
- * revision.
95
- */
96
- export declare const FLASH_SERIES_RATE_CHANGE_AT: number;
97
- /**
98
- * Inclusive epoch ms of the announced V4 Pro route switch: 2026-09-14 12:00
99
- * Beijing time (UTC+8, no DST) = 04:00 UTC. From that instant the V4 Pro route
100
- * is served by V4.1 Flash and billed at the V4.1 Flash rates.
101
- */
102
- export declare const V4_PRO_ROUTE_SWITCH_AT: number;
103
- /**
104
- * Official peak/off-peak rates (CNY per 1M tokens) per model, as dated
105
- * revisions. Base rows are the schedule effective 2026-08-17; the V4 Flash
106
- * series (V4.1 Flash, V4 Flash, V4 Flash Vision Exp) carries the second
107
- * revision effective 2026-09-10 12:00 Beijing, and the V4 Pro row the V4.1
108
- * Flash rates from its announced route switch (2026-09-14 12:00 Beijing) —
109
- * the MiMo series (V2.5, and V2.6 which kept V2.5's pricing) is untouched by
110
- * either adjustment. Rows sharing a model are that model's rate history.
111
- */
112
- export declare const DEFAULT_MODEL_PRICING: readonly BillingConfigModel[];
113
- /** Resolved billing configuration: a pricing table plus peak-hour windows. */
114
- export interface ResolvedBilling {
115
- peakHours: readonly {
116
- start: number;
117
- end: number;
118
- }[];
119
- models: ReadonlyMap<string, DeepSeekModelPricing>;
120
- }
121
- /**
122
- * Resolve optional configuration to a pricing table, defaulting omitted or
123
- * empty rows to the published rates. Schemastery materializes an absent
124
- * `z.array` as `[]` rather than `undefined`, so emptiness — not just absence —
125
- * selects the defaults. Explicit non-empty rows override the same model; a
126
- * supplied non-empty `models` list is authoritative.
127
- *
128
- * Rows sharing a model are that model's rate revisions, kept in ascending
129
- * `effectiveFrom` order (an undated base revision first). Two rows declaring
130
- * the same effective instant are one revision and the later row wins — the
131
- * historical override rule — so re-declaring a model can neither duplicate a
132
- * revision nor install a second undated base.
133
- * @param config - optional raw billing configuration.
134
- * @returns the resolved table (per model: its revisions plus the newest rates) and peak-hour windows.
135
- */
136
- export declare function resolveBilling(config: BillingConfig | undefined): ResolvedBilling;
137
- /** One shifted-timestamp view of a Beijing (UTC+8, no DST) instant. */
138
- export interface BeijingParts {
139
- /**
140
- * The instant in epoch milliseconds. Pricing needs it back to resolve the
141
- * rate revision in effect at the sample's own timestamp (see
142
- * {@link ratesAt}), so the view carries it instead of a second parse.
143
- */
144
- time: number;
145
- /** Beijing hour, `0`–`23`. */
146
- hour: number;
147
- /** Beijing weekday as `getUTCDay()`: `0` is Sunday, `6` is Saturday. */
148
- weekday: number;
149
- /** Beijing calendar-day key (`YYYY-MM-DD`). */
150
- dayKey: string;
151
- }
152
- /**
153
- * Derive the Beijing hour, weekday, and calendar-day key of one timestamp with
154
- * pure integer arithmetic — every timezone-sensitive read shares this one
155
- * implementation, so the pieces cannot drift apart. Callers that filter by
156
- * day and then price the same event reuse the returned view, so each event is
157
- * parsed exactly once. (The hot fold path runs this per committed event; the
158
- * previous `Date` + `toISOString().slice()` version allocated a `Date` and a
159
- * 24-character string per call.)
160
- * @param time - epoch milliseconds.
161
- * @throws {RangeError} when `time` is not a finite number.
162
- */
163
- export declare function beijingPartsOf(time: number): BeijingParts;
164
- /** The Beijing (Asia/Shanghai, UTC+8, no DST) calendar-day key of a timestamp. */
165
- export declare function beijingDayKey(now: Date): string;
15
+ import type { ResolvedBilling } from './pricing.ts';
16
+ export * from './beijing-time.ts';
17
+ export * from './spend.ts';
18
+ export * from './pricing.ts';
19
+ export * from './fold.ts';
166
20
  /**
167
21
  * Structural source of a session's durable inherited-prefix boundary. The
168
- * field moved between DSH runtimes:
169
- *
170
- * - since 0.1.2-alpha.4, `SessionHeader.seedLength` was removed (the header
171
- * now carries only `isSeeded`: boolean) and the exact cut moved to
172
- * `Session.inheritedEventCount` / `SessionInspection.inheritedEventCount`;
173
- * - at and before the 0.1.1-rc.2 npm baseline, the cut lived on the durable
174
- * header as `seedLength` (optional; absent for an unseeded session) and on
175
- * the persistence `inspect` result as `meta.seedLength`.
176
- *
177
- * The reader accepts any of the three shapes and prefers the newer,
178
- * exact-count field, so the same plugin code prices correctly on both
179
- * families of runtime.
22
+ * exact cut moved between DSH runtimes: since 0.1.2-alpha.4,
23
+ * `SessionHeader.seedLength` was removed (the header now carries only
24
+ * `isSeeded`: boolean) and the count moved to `Session.inheritedEventCount` /
25
+ * `SessionHandle.inheritedEventCount`. The reader prefers that exact-count
26
+ * field and otherwise falls back to the older `seedLength` on the durable
27
+ * header, so the same plugin code prices correctly across the live-session
28
+ * and handle surfaces it still meets.
180
29
  */
181
30
  export interface ForkBoundarySource {
182
- /** 0.1.2-alpha.4+: `Session.inheritedEventCount` / `SessionInspection.inheritedEventCount`. */
31
+ /** 0.1.2-alpha.4+: `Session.inheritedEventCount` / `SessionHandle.inheritedEventCount`. */
183
32
  readonly inheritedEventCount?: number;
184
- /** 0.1.1-rc.2 and earlier: `SessionHeader.seedLength`. */
33
+ /** ≤ 0.1.1-rc.2 live-session header: `SessionHeader.seedLength`. */
185
34
  readonly seedLength?: number;
186
- /** ≤ 0.1.1-rc.2: the header slice of a `Session` (its `header.seedLength`). */
35
+ /** Durable header slice of a live session or opened handle (its `seedLength`). */
187
36
  readonly header?: {
188
37
  readonly seedLength?: number;
189
38
  readonly isSeeded?: boolean;
190
39
  };
191
- /** ≤ 0.1.1-rc.2: the `meta` of a persistence `inspect` result (its `seedLength`). */
192
- readonly meta?: {
193
- readonly seedLength?: number;
194
- readonly isSeeded?: boolean;
195
- };
196
40
  }
197
41
  /**
198
42
  * The durable inherited-prefix boundary of one session: the number of leading
@@ -201,222 +45,17 @@ export interface ForkBoundarySource {
201
45
  * in its session state; every event with `seq < seedLength` is a copy of an
202
46
  * event already billed in that source session, so pricing must skip them or
203
47
  * the same model output is counted once per copy. Accepts the durable field
204
- * of both DSH runtime families (see {@link ForkBoundarySource}).
205
- * @param source - the session, inspection result, header slice, or durable
206
- * header carrying the boundary; `undefined` reads as 0.
48
+ * of the live-session and handle surfaces (see {@link ForkBoundarySource}).
49
+ * @param source - the session, header slice, or opened handle carrying the
50
+ * boundary; `undefined` reads as 0.
207
51
  * @returns the inherited-prefix length; 0 for an unseeded session.
208
52
  */
209
53
  export declare function forkBoundaryOf(source: ForkBoundarySource | undefined): number;
210
- /** Whether a durable header marks a fork-inherited (seeded) session across both runtime families. */
54
+ /** Whether a durable header marks a fork-inherited (seeded) session across the live-session and handle surfaces. */
211
55
  export declare function isSeededSession(header: {
212
56
  readonly seedLength?: number;
213
57
  readonly isSeeded?: boolean;
214
58
  } | undefined): boolean;
215
- /**
216
- * Whether a timestamp falls inside any peak-hour window (Beijing time,
217
- * weekdays Monday–Friday only). Weekends (Saturday and Sunday) are always
218
- * off-peak, matching the published peak-hours rule.
219
- * @param billing - resolved pricing with peak-hour windows.
220
- * @param now - the moment to classify.
221
- * @returns true during a weekday peak hour.
222
- */
223
- export declare function isPeak(billing: ResolvedBilling, now: Date): boolean;
224
- /**
225
- * One priced billed event's contribution: the model row plus the Beijing
226
- * calendar day its timestamp falls on. `undefined` when the event carries no
227
- * priced usage (not an `assistant/message`, no usage report, or a model
228
- * without a pricing row).
229
- */
230
- export interface BillingEventContribution {
231
- /** Beijing-time calendar-day key of the event's timestamp. */
232
- dayKey: string;
233
- /** Wire model id, e.g. `deepseek-v4-flash`. */
234
- model: string;
235
- /** Selector label for the model. */
236
- displayName: string;
237
- /** Billed cost in CNY (peak plus off-peak portions). */
238
- cost: number;
239
- /** Cost portion billed at peak rates. */
240
- peakCost: number;
241
- /** Cost portion billed at off-peak rates. */
242
- offPeakCost: number;
243
- /** Cache-hit input tokens billed at the hit rate. */
244
- cacheHitInputTokens: number;
245
- /** Cache-miss input tokens (uncached input plus cache writes), billed at the miss rate. */
246
- cacheMissInputTokens: number;
247
- /** Output tokens (reasoning included), billed at the output rate. */
248
- outputTokens: number;
249
- /** Billed cost of cache-hit input tokens in CNY. */
250
- cacheHitInputCost: number;
251
- /** Billed cost of cache-miss input tokens (uncached input plus cache writes) in CNY. */
252
- cacheMissInputCost: number;
253
- /** Billed cost of output tokens (reasoning included) in CNY. */
254
- outputCost: number;
255
- }
256
- /**
257
- * Price one event at the official per-model rates, applying the peak/off-peak
258
- * table by its Beijing-time hour and weekday (peak windows apply Monday–Friday
259
- * only; weekends are off-peak) and the rate revision in effect at its own
260
- * timestamp. Each `assistant/message` event with usage
261
- * contributes cache-hit input, cache-miss input (uncached input plus cache
262
- * writes), and output (reasoning included) tokens at the rate of its own
263
- * timestamp; a model with usage but no pricing row contributes nothing.
264
- * @param event - the event to price.
265
- * @param billing - resolved pricing with peak-hour windows.
266
- * @param names - model id → display label.
267
- * @returns the priced contribution, or `undefined` when the event has no priced usage.
268
- */
269
- export declare function priceEvent(event: SessionEvent, billing: ResolvedBilling, names: ReadonlyMap<string, string>): BillingEventContribution | undefined;
270
- /**
271
- * Price one event at the official per-model rates using a precomputed
272
- * Beijing-time view — the day-filtering and pricing of one event share a
273
- * single timezone parse (see {@link beijingPartsOf}). Semantics are identical
274
- * to {@link priceEvent}.
275
- * @param parts - the event's Beijing-time view.
276
- * @param event - the event to price.
277
- * @param billing - resolved pricing with peak-hour windows.
278
- * @param names - model id → display label.
279
- * @returns the priced contribution, or `undefined` when the event has no priced usage.
280
- */
281
- export declare function priceEventAt(parts: BeijingParts, event: SessionEvent, billing: ResolvedBilling, names: ReadonlyMap<string, string>): BillingEventContribution | undefined;
282
- /**
283
- * Price one provider-reported usage sample for one model at the rates of the
284
- * sample's own Beijing-time hour and weekday — the peak or off-peak price of
285
- * the rate revision in effect at the sample's own timestamp (a re-priced series
286
- * bills its history at the rates that applied then). `undefined` when the model
287
- * has no pricing row.
288
- * @param parts - the sample's Beijing-time view.
289
- * @param usage - the reported token buckets.
290
- * @param model - the wire model id the sample belongs to.
291
- * @param billing - resolved pricing with peak-hour windows.
292
- * @param names - model id → display label.
293
- * @returns the priced contribution, or `undefined` when the model has no rate row.
294
- */
295
- export declare function priceUsage(parts: BeijingParts, usage: TokenUsage, model: string, billing: ResolvedBilling, names: ReadonlyMap<string, string>): BillingEventContribution | undefined;
296
- /** A spend with no priced usage. */
297
- export declare function emptyTodaySpend(): DeepSeekTodaySpend;
298
- /**
299
- * Mutable model-row accumulator behind every spend fold. Rows keep first-seen
300
- * model order — the same shape a pure `addEventContribution` chain produces —
301
- * so the single-pass scan path and the pure public paths cannot diverge. One
302
- * `Map` lookup per contribution instead of a per-event array copy: the huge
303
- * event-log folds allocate one row object per model, not one intermediate
304
- * array per event.
305
- */
306
- export declare class SpendAccumulator {
307
- private readonly rows;
308
- private total;
309
- /** Add one priced contribution. */
310
- add(priced: BillingEventContribution): void;
311
- /** The folded spend; the accumulator stays usable afterwards. */
312
- finish(): DeepSeekTodaySpend;
313
- }
314
- /** The additive inverse of one spend (pure): used to replace a priced sample. */
315
- export declare function negateSpend(spend: DeepSeekTodaySpend): DeepSeekTodaySpend;
316
- /**
317
- * Subtract one spend from another (pure). Rows that cancel out completely are
318
- * dropped so a replaced sample leaves no zero row behind.
319
- * @param target - the spend to subtract from.
320
- * @param source - the spend to remove.
321
- * @returns the difference.
322
- */
323
- export declare function subtractSpend(target: DeepSeekTodaySpend, source: DeepSeekTodaySpend): DeepSeekTodaySpend;
324
- /**
325
- * One priced attempt sample kept for same-step replacement: DSH can report the
326
- * same `(turn, step)` twice (an `assistant/attempt` stream and the
327
- * `assistant/message` that assembles from it), and a later sample replaces the
328
- * earlier one instead of adding to it. `llm/retry-started` clears the slot, so
329
- * a retried attempt adds rather than replaces (both requests were billed).
330
- */
331
- export interface BillingFoldSample {
332
- /** Turn of the producing attempt. */
333
- turn: number;
334
- /** Step of the producing attempt. */
335
- step: number;
336
- /** Beijing day of the sample's timestamp. */
337
- dayKey: string;
338
- /** The sample's contribution as a one-row spend (subtracted on replacement). */
339
- spend: DeepSeekTodaySpend;
340
- }
341
- /**
342
- * Plain-JSON fold state of one session's billed spend: the latest priced day,
343
- * the whole-session total, the fork boundary, the model of the latest request
344
- * (needed to price an `assistant/attempt`, which carries no route), and the
345
- * last sample kept for replacement.
346
- */
347
- export interface BillingFoldState {
348
- /** Beijing-time calendar-day key of `spend`; `''` for no priced usage. */
349
- dayKey: string;
350
- /** The spend of the session's latest priced Beijing day (own events only). */
351
- spend: DeepSeekTodaySpend;
352
- /** The spend of the session's OWN events across every day. */
353
- session: DeepSeekTodaySpend;
354
- /** Fork-inherited prefix length; events below it belong to the source session. */
355
- inheritedEventCount: number;
356
- /** Wire model of the latest `request/header`; `''` before the first one. */
357
- model: string;
358
- /** Latest priced attempt sample, for same-step replacement. */
359
- last: BillingFoldSample | null;
360
- }
361
- /** The empty fold state for one fork boundary. */
362
- export declare function emptyBillingFoldState(inheritedEventCount?: number): BillingFoldState;
363
- /**
364
- * Fold one committed event into a session's billed-spend state.
365
- *
366
- * Priced samples come from `assistant/message` (its own reported usage, or the
367
- * stream's last usage chunk) and `assistant/attempt` (the stream's last usage
368
- * chunk, priced with the model of the latest `request/header`, since an
369
- * attempt carries no route). A sample for the same `(turn, step)` replaces the
370
- * previous one; `llm/retry-started` closes the replacement slot so a retried
371
- * attempt adds. Every other event is inert and returns the same state
372
- * reference.
373
- * @param state - the previous fold state.
374
- * @param event - the committed event.
375
- * @param billing - resolved pricing with peak-hour windows.
376
- * @param names - model id → display label.
377
- * @returns the next state (the same reference when nothing was priced).
378
- */
379
- export declare function applyBillingEvent(state: BillingFoldState, event: SessionEvent, billing: ResolvedBilling, names: ReadonlyMap<string, string>): BillingFoldState;
380
- /**
381
- * Mutable wrapper over {@link applyBillingEvent} for the pure pricing paths:
382
- * feed events in order, read the folded spend.
383
- */
384
- export declare class BillingFolder {
385
- private readonly billing;
386
- private state;
387
- /**
388
- * @param billing - resolved pricing with peak-hour windows.
389
- * @param catalog - model display rows, in presentation order.
390
- * @param inheritedEventCount - fork boundary to skip (default 0).
391
- */
392
- constructor(billing: ResolvedBilling, catalog: readonly {
393
- id: string;
394
- name: string;
395
- }[], inheritedEventCount?: number);
396
- private readonly names;
397
- /** Fold one event. */
398
- add(event: SessionEvent): void;
399
- /** Fold every event, in order. */
400
- addAll(events: readonly SessionEvent[]): void;
401
- /** The folded state (live reference; do not mutate). */
402
- get fold(): BillingFoldState;
403
- }
404
- /**
405
- * Merge one priced event's contribution into an accumulator spend (pure:
406
- * returns a new spend, never mutates its input).
407
- * @param spend - the accumulator (per session and day, or across sessions).
408
- * @param priced - the priced contribution to add.
409
- * @returns the merged spend.
410
- */
411
- export declare function addEventContribution(spend: DeepSeekTodaySpend, priced: BillingEventContribution): DeepSeekTodaySpend;
412
- /**
413
- * Sum two spends (per session and day, or across sessions) into one (pure:
414
- * returns a new spend, never mutates its inputs).
415
- * @param target - the accumulator spend.
416
- * @param source - the spend to add.
417
- * @returns the summed spend.
418
- */
419
- export declare function mergeTodaySpend(target: DeepSeekTodaySpend, source: DeepSeekTodaySpend): DeepSeekTodaySpend;
420
59
  /**
421
60
  * Price one session's complete event log at the official per-model rates,
422
61
  * with DSH's attempt semantics: every provider-reported sample (an