@rayadesu/dsh-llm-billing 0.3.7 → 0.3.9

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.
@@ -12,8 +12,9 @@
12
12
  * from the others.
13
13
  * @module @rayadesu/dsh-llm-billing/billing
14
14
  */
15
+ import type { TokenUsage } from '@deepseek-ai/dsh-llm';
15
16
  import type { SessionEvent } from '@deepseek-ai/dsh-session';
16
- import type { DeepSeekSessionSpend, DeepSeekTodaySpend, DeepSeekTurnSpend } from './types.ts';
17
+ import type { DeepSeekSessionSpend, DeepSeekSessionTurnSpends, DeepSeekTodaySpend, DeepSeekTurnSpend } from './types.ts';
17
18
  /** One token price point, in CNY per 1M tokens. */
18
19
  export interface DeepSeekTokenPrice {
19
20
  /** 1M input cache-hit tokens. */
@@ -53,7 +54,7 @@ export interface BillingConfig {
53
54
  * only; weekends (Saturday and Sunday) are always off-peak.
54
55
  */
55
56
  peakHours?: PeakHourWindow[];
56
- /** Per-model pricing rows; omission uses the V4 Flash, V4 Pro, and V4 Flash Vision defaults. */
57
+ /** Per-model pricing rows; omission uses the V4 Flash, V4.1 Flash, V4 Pro, and V4 Flash Vision defaults. */
57
58
  models?: BillingConfigModel[];
58
59
  }
59
60
  /**
@@ -61,12 +62,9 @@ export interface BillingConfig {
61
62
  * applied on weekdays (Monday–Friday) only — weekends are always off-peak
62
63
  * (effective 2026-08-23).
63
64
  */
64
- export declare const DEFAULT_PEAK_HOURS: {
65
- start: number;
66
- end: number;
67
- }[];
65
+ export declare const DEFAULT_PEAK_HOURS: readonly PeakHourWindow[];
68
66
  /** Official peak/off-peak rates (CNY per 1M tokens), effective 2026-08-17. */
69
- export declare const DEFAULT_MODEL_PRICING: BillingConfigModel[];
67
+ export declare const DEFAULT_MODEL_PRICING: readonly BillingConfigModel[];
70
68
  /** Resolved billing configuration: a pricing table plus peak-hour windows. */
71
69
  export interface ResolvedBilling {
72
70
  peakHours: readonly {
@@ -85,6 +83,27 @@ export interface ResolvedBilling {
85
83
  * @returns the resolved table and peak-hour windows.
86
84
  */
87
85
  export declare function resolveBilling(config: BillingConfig | undefined): ResolvedBilling;
86
+ /** One shifted-timestamp view of a Beijing (UTC+8, no DST) instant. */
87
+ export interface BeijingParts {
88
+ /** Beijing hour, `0`–`23`. */
89
+ hour: number;
90
+ /** Beijing weekday as `getUTCDay()`: `0` is Sunday, `6` is Saturday. */
91
+ weekday: number;
92
+ /** Beijing calendar-day key (`YYYY-MM-DD`). */
93
+ dayKey: string;
94
+ }
95
+ /**
96
+ * Derive the Beijing hour, weekday, and calendar-day key of one timestamp with
97
+ * pure integer arithmetic — every timezone-sensitive read shares this one
98
+ * implementation, so the pieces cannot drift apart. Callers that filter by
99
+ * day and then price the same event reuse the returned view, so each event is
100
+ * parsed exactly once. (The hot fold path runs this per committed event; the
101
+ * previous `Date` + `toISOString().slice()` version allocated a `Date` and a
102
+ * 24-character string per call.)
103
+ * @param time - epoch milliseconds.
104
+ * @throws {RangeError} when `time` is not a finite number.
105
+ */
106
+ export declare function beijingPartsOf(time: number): BeijingParts;
88
107
  /** The Beijing (Asia/Shanghai, UTC+8, no DST) calendar-day key of a timestamp. */
89
108
  export declare function beijingDayKey(now: Date): string;
90
109
  /**
@@ -183,14 +202,37 @@ export interface BillingEventContribution {
183
202
  * only; weekends are off-peak). Each `assistant/message` event with usage
184
203
  * contributes cache-hit input, cache-miss input (uncached input plus cache
185
204
  * writes), and output (reasoning included) tokens at the rate of its own
186
- * timestamp; a model with usage but no pricing row contributes nothing (the
187
- * published table prices only the two V4 rows).
205
+ * timestamp; a model with usage but no pricing row contributes nothing.
188
206
  * @param event - the event to price.
189
207
  * @param billing - resolved pricing with peak-hour windows.
190
208
  * @param names - model id → display label.
191
209
  * @returns the priced contribution, or `undefined` when the event has no priced usage.
192
210
  */
193
211
  export declare function priceEvent(event: SessionEvent, billing: ResolvedBilling, names: ReadonlyMap<string, string>): BillingEventContribution | undefined;
212
+ /**
213
+ * Price one event at the official per-model rates using a precomputed
214
+ * Beijing-time view — the day-filtering and pricing of one event share a
215
+ * single timezone parse (see {@link beijingPartsOf}). Semantics are identical
216
+ * to {@link priceEvent}.
217
+ * @param parts - the event's Beijing-time view.
218
+ * @param event - the event to price.
219
+ * @param billing - resolved pricing with peak-hour windows.
220
+ * @param names - model id → display label.
221
+ * @returns the priced contribution, or `undefined` when the event has no priced usage.
222
+ */
223
+ export declare function priceEventAt(parts: BeijingParts, event: SessionEvent, billing: ResolvedBilling, names: ReadonlyMap<string, string>): BillingEventContribution | undefined;
224
+ /**
225
+ * Price one provider-reported usage sample for one model at the rates of the
226
+ * sample's own Beijing-time hour and weekday. `undefined` when the model has
227
+ * no pricing row.
228
+ * @param parts - the sample's Beijing-time view.
229
+ * @param usage - the reported token buckets.
230
+ * @param model - the wire model id the sample belongs to.
231
+ * @param billing - resolved pricing with peak-hour windows.
232
+ * @param names - model id → display label.
233
+ * @returns the priced contribution, or `undefined` when the model has no rate row.
234
+ */
235
+ export declare function priceUsage(parts: BeijingParts, usage: TokenUsage, model: string, billing: ResolvedBilling, names: ReadonlyMap<string, string>): BillingEventContribution | undefined;
194
236
  /** A spend with no priced usage. */
195
237
  export declare function emptyTodaySpend(): DeepSeekTodaySpend;
196
238
  /**
@@ -209,6 +251,96 @@ export declare class SpendAccumulator {
209
251
  /** The folded spend; the accumulator stays usable afterwards. */
210
252
  finish(): DeepSeekTodaySpend;
211
253
  }
254
+ /** The additive inverse of one spend (pure): used to replace a priced sample. */
255
+ export declare function negateSpend(spend: DeepSeekTodaySpend): DeepSeekTodaySpend;
256
+ /**
257
+ * Subtract one spend from another (pure). Rows that cancel out completely are
258
+ * dropped so a replaced sample leaves no zero row behind.
259
+ * @param target - the spend to subtract from.
260
+ * @param source - the spend to remove.
261
+ * @returns the difference.
262
+ */
263
+ export declare function subtractSpend(target: DeepSeekTodaySpend, source: DeepSeekTodaySpend): DeepSeekTodaySpend;
264
+ /**
265
+ * One priced attempt sample kept for same-step replacement: DSH can report the
266
+ * same `(turn, step)` twice (an `assistant/attempt` stream and the
267
+ * `assistant/message` that assembles from it), and a later sample replaces the
268
+ * earlier one instead of adding to it. `llm/retry-started` clears the slot, so
269
+ * a retried attempt adds rather than replaces (both requests were billed).
270
+ */
271
+ export interface BillingFoldSample {
272
+ /** Turn of the producing attempt. */
273
+ turn: number;
274
+ /** Step of the producing attempt. */
275
+ step: number;
276
+ /** Beijing day of the sample's timestamp. */
277
+ dayKey: string;
278
+ /** The sample's contribution as a one-row spend (subtracted on replacement). */
279
+ spend: DeepSeekTodaySpend;
280
+ }
281
+ /**
282
+ * Plain-JSON fold state of one session's billed spend: the latest priced day,
283
+ * the whole-session total, the fork boundary, the model of the latest request
284
+ * (needed to price an `assistant/attempt`, which carries no route), and the
285
+ * last sample kept for replacement.
286
+ */
287
+ export interface BillingFoldState {
288
+ /** Beijing-time calendar-day key of `spend`; `''` for no priced usage. */
289
+ dayKey: string;
290
+ /** The spend of the session's latest priced Beijing day (own events only). */
291
+ spend: DeepSeekTodaySpend;
292
+ /** The spend of the session's OWN events across every day. */
293
+ session: DeepSeekTodaySpend;
294
+ /** Fork-inherited prefix length; events below it belong to the source session. */
295
+ inheritedEventCount: number;
296
+ /** Wire model of the latest `request/header`; `''` before the first one. */
297
+ model: string;
298
+ /** Latest priced attempt sample, for same-step replacement. */
299
+ last: BillingFoldSample | null;
300
+ }
301
+ /** The empty fold state for one fork boundary. */
302
+ export declare function emptyBillingFoldState(inheritedEventCount?: number): BillingFoldState;
303
+ /**
304
+ * Fold one committed event into a session's billed-spend state.
305
+ *
306
+ * Priced samples come from `assistant/message` (its own reported usage, or the
307
+ * stream's last usage chunk) and `assistant/attempt` (the stream's last usage
308
+ * chunk, priced with the model of the latest `request/header`, since an
309
+ * attempt carries no route). A sample for the same `(turn, step)` replaces the
310
+ * previous one; `llm/retry-started` closes the replacement slot so a retried
311
+ * attempt adds. Every other event is inert and returns the same state
312
+ * reference.
313
+ * @param state - the previous fold state.
314
+ * @param event - the committed event.
315
+ * @param billing - resolved pricing with peak-hour windows.
316
+ * @param names - model id → display label.
317
+ * @returns the next state (the same reference when nothing was priced).
318
+ */
319
+ export declare function applyBillingEvent(state: BillingFoldState, event: SessionEvent, billing: ResolvedBilling, names: ReadonlyMap<string, string>): BillingFoldState;
320
+ /**
321
+ * Mutable wrapper over {@link applyBillingEvent} for the pure pricing paths:
322
+ * feed events in order, read the folded spend.
323
+ */
324
+ export declare class BillingFolder {
325
+ private readonly billing;
326
+ private state;
327
+ /**
328
+ * @param billing - resolved pricing with peak-hour windows.
329
+ * @param catalog - model display rows, in presentation order.
330
+ * @param inheritedEventCount - fork boundary to skip (default 0).
331
+ */
332
+ constructor(billing: ResolvedBilling, catalog: readonly {
333
+ id: string;
334
+ name: string;
335
+ }[], inheritedEventCount?: number);
336
+ private readonly names;
337
+ /** Fold one event. */
338
+ add(event: SessionEvent): void;
339
+ /** Fold every event, in order. */
340
+ addAll(events: readonly SessionEvent[]): void;
341
+ /** The folded state (live reference; do not mutate). */
342
+ get fold(): BillingFoldState;
343
+ }
212
344
  /**
213
345
  * Merge one priced event's contribution into an accumulator spend (pure:
214
346
  * returns a new spend, never mutates its input).
@@ -226,7 +358,11 @@ export declare function addEventContribution(spend: DeepSeekTodaySpend, priced:
226
358
  */
227
359
  export declare function mergeTodaySpend(target: DeepSeekTodaySpend, source: DeepSeekTodaySpend): DeepSeekTodaySpend;
228
360
  /**
229
- * Price one session's complete event log at the official per-model rates.
361
+ * Price one session's complete event log at the official per-model rates,
362
+ * with DSH's attempt semantics: every provider-reported sample (an
363
+ * `assistant/message`'s usage, or an `assistant/attempt`'s stream usage)
364
+ * contributes, a later sample for the same `(turn, step)` replaces the earlier
365
+ * one, and `llm/retry-started` makes the retried attempt add.
230
366
  * @param events - one session's complete event log.
231
367
  * @param billing - resolved pricing with peak-hour windows.
232
368
  * @param catalog - model display rows, in presentation order.
@@ -241,13 +377,13 @@ export declare function computeSessionSpend(events: readonly SessionEvent[], bil
241
377
  name: string;
242
378
  }[], startSeq?: number): DeepSeekSessionSpend;
243
379
  /**
244
- * Price one completed Turn's billed usage at the official per-model rates,
245
- * identified by its closing assistant message id. The turn's events are those
246
- * between its `turn/start` and `turn/end` (both matched by the message's own
247
- * turn coordinate); each priced event applies the peak/off-peak table by its
248
- * Beijing-time hour and weekday. A message that cannot be located, a turn
249
- * without bracketing `turn/start` / `turn/end` events (for example after
250
- * compaction), or a session with no priced usage prices to zero.
380
+ * Price one completed Turn's billed usage, identified by its closing
381
+ * assistant message id. The turn's events are those between its `turn/start`
382
+ * and `turn/end` (both matched by the message's own turn coordinate), priced
383
+ * with the same attempt semantics as {@link computeSessionSpend}. A message
384
+ * that cannot be located, a turn without bracketing `turn/start` / `turn/end`
385
+ * events (for example after compaction), or a session with no priced usage
386
+ * prices to zero.
251
387
  * @param events - one session's complete event log.
252
388
  * @param billing - resolved pricing with peak-hour windows.
253
389
  * @param catalog - model display rows, in presentation order.
@@ -259,10 +395,70 @@ export declare function computeTurnSpend(events: readonly SessionEvent[], billin
259
395
  name: string;
260
396
  }[], messageId: string): DeepSeekTurnSpend;
261
397
  /**
262
- * Price every event whose Beijing-time calendar day is the day of `now`,
263
- * aggregating across every session's event log. Events from other Beijing
264
- * days are ignored, so a caller passes the concatenated logs of all sessions.
265
- * @param events - every session's complete event log, concatenated.
398
+ * Incremental single-pass fold of one session's completed-Turn costs, keyed by
399
+ * the id of every assistant message inside each Turn. Feeding the fold only
400
+ * the appended tail keeps a growing session's map current in O(new events)
401
+ * instead of re-scanning the whole log per message.
402
+ *
403
+ * Semantics are exactly {@link computeTurnSpend}'s: a Turn is the
404
+ * `turn/start`..`turn/end` range (matched by the event's own turn coordinate),
405
+ * every priced event inside it contributes at its own timestamp's rate, and a
406
+ * message outside any bracket contributes nothing.
407
+ */
408
+ export declare class SessionTurnSpendFolder {
409
+ private readonly billing;
410
+ private readonly catalog;
411
+ private readonly rows;
412
+ private ids;
413
+ /** Events of the open Turn, folded with the shared attempt semantics on close. */
414
+ private events;
415
+ private open;
416
+ /** Events already fed; a shorter log resets the fold. */
417
+ private cursor;
418
+ /**
419
+ * @param billing - resolved pricing with peak-hour windows.
420
+ * @param catalog - model display rows, in presentation order.
421
+ */
422
+ constructor(billing: ResolvedBilling, catalog: readonly {
423
+ id: string;
424
+ name: string;
425
+ }[]);
426
+ /** How many events have been folded so far (the host's incremental cursor). */
427
+ get processed(): number;
428
+ /**
429
+ * Fold every event from the cursor to the end of the log. A log shorter than
430
+ * the cursor (rewritten session) restarts the fold from an empty state.
431
+ * @param events - the session's complete event log, in seq order.
432
+ */
433
+ feed(events: readonly SessionEvent[]): void;
434
+ /** The folded map; the fold stays usable afterwards. */
435
+ finish(): DeepSeekSessionTurnSpends;
436
+ /** Drop the fold state so the next feed starts from the log's beginning. */
437
+ private reset;
438
+ }
439
+ /**
440
+ * Price every completed Turn of one session in a single pass (the pure
441
+ * equivalent of {@link SessionTurnSpendFolder}).
442
+ * @param events - one session's complete event log.
443
+ * @param billing - resolved pricing with peak-hour windows.
444
+ * @param catalog - model display rows, in presentation order.
445
+ * @returns one row per assistant message inside a completed Turn, in log order.
446
+ */
447
+ export declare function computeSessionTurnSpends(events: readonly SessionEvent[], billing: ResolvedBilling, catalog: readonly {
448
+ id: string;
449
+ name: string;
450
+ }[]): DeepSeekSessionTurnSpends;
451
+ /**
452
+ * Price one session's log for the Beijing-time calendar day of `now`. Events
453
+ * after the reference day are ignored; the fold's latest-day state then
454
+ * answers the query exactly (empty when the session's latest priced day is not
455
+ * the reference day). Pricing follows {@link applyBillingEvent} (attempt
456
+ * samples with same-step replacement).
457
+ *
458
+ * The fold's `(turn, step)` replacement slot is per session, so callers must
459
+ * pass ONE session's log; aggregate across sessions with
460
+ * {@link mergeTodaySpend}.
461
+ * @param events - one session's complete event log.
266
462
  * @param billing - resolved pricing with peak-hour windows.
267
463
  * @param catalog - model display rows, in presentation order.
268
464
  * @param now - the reference moment whose Beijing-time calendar day is "today".