@rayadesu/dsh-llm-billing 0.3.8 → 0.3.10

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. */
@@ -23,21 +24,41 @@ export interface DeepSeekTokenPrice {
23
24
  /** 1M output tokens. */
24
25
  output: number;
25
26
  }
26
- /** Peak and off-peak price pair for one model. */
27
- export interface DeepSeekModelPricing {
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 {
28
34
  /** Price during peak hours. */
29
35
  peak: DeepSeekTokenPrice;
30
36
  /** Price during off-peak hours. */
31
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[];
32
57
  }
33
58
  /** One model's pricing-table row in configuration form. */
34
- export interface BillingConfigModel {
59
+ export interface BillingConfigModel extends DeepSeekRateRevision {
35
60
  /** Wire model id. */
36
61
  model: string;
37
- /** Peak-hour price. */
38
- peak: DeepSeekTokenPrice;
39
- /** Off-peak price. */
40
- offPeak: DeepSeekTokenPrice;
41
62
  }
42
63
  /** One peak-hour window on a 24h Beijing-time clock, applied weekdays only. */
43
64
  export interface PeakHourWindow {
@@ -53,7 +74,11 @@ export interface BillingConfig {
53
74
  * only; weekends (Saturday and Sunday) are always off-peak.
54
75
  */
55
76
  peakHours?: PeakHourWindow[];
56
- /** Per-model pricing rows; omission uses the V4 Flash, V4 Pro, and V4 Flash Vision defaults. */
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
+ */
57
82
  models?: BillingConfigModel[];
58
83
  }
59
84
  /**
@@ -61,12 +86,30 @@ export interface BillingConfig {
61
86
  * applied on weekdays (Monday–Friday) only — weekends are always off-peak
62
87
  * (effective 2026-08-23).
63
88
  */
64
- export declare const DEFAULT_PEAK_HOURS: {
65
- start: number;
66
- end: number;
67
- }[];
68
- /** Official peak/off-peak rates (CNY per 1M tokens), effective 2026-08-17. */
69
- export declare const DEFAULT_MODEL_PRICING: BillingConfigModel[];
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-V2.5 series is untouched by either adjustment. Rows sharing a model
110
+ * are that model's rate history.
111
+ */
112
+ export declare const DEFAULT_MODEL_PRICING: readonly BillingConfigModel[];
70
113
  /** Resolved billing configuration: a pricing table plus peak-hour windows. */
71
114
  export interface ResolvedBilling {
72
115
  peakHours: readonly {
@@ -81,10 +124,43 @@ export interface ResolvedBilling {
81
124
  * `z.array` as `[]` rather than `undefined`, so emptiness — not just absence —
82
125
  * selects the defaults. Explicit non-empty rows override the same model; a
83
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.
84
133
  * @param config - optional raw billing configuration.
85
- * @returns the resolved table and peak-hour windows.
134
+ * @returns the resolved table (per model: its revisions plus the newest rates) and peak-hour windows.
86
135
  */
87
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;
88
164
  /** The Beijing (Asia/Shanghai, UTC+8, no DST) calendar-day key of a timestamp. */
89
165
  export declare function beijingDayKey(now: Date): string;
90
166
  /**
@@ -180,17 +256,43 @@ export interface BillingEventContribution {
180
256
  /**
181
257
  * Price one event at the official per-model rates, applying the peak/off-peak
182
258
  * table by its Beijing-time hour and weekday (peak windows apply Monday–Friday
183
- * only; weekends are off-peak). Each `assistant/message` event with usage
259
+ * only; weekends are off-peak) and the rate revision in effect at its own
260
+ * timestamp. Each `assistant/message` event with usage
184
261
  * contributes cache-hit input, cache-miss input (uncached input plus cache
185
262
  * 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).
263
+ * timestamp; a model with usage but no pricing row contributes nothing.
188
264
  * @param event - the event to price.
189
265
  * @param billing - resolved pricing with peak-hour windows.
190
266
  * @param names - model id → display label.
191
267
  * @returns the priced contribution, or `undefined` when the event has no priced usage.
192
268
  */
193
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;
194
296
  /** A spend with no priced usage. */
195
297
  export declare function emptyTodaySpend(): DeepSeekTodaySpend;
196
298
  /**
@@ -209,6 +311,96 @@ export declare class SpendAccumulator {
209
311
  /** The folded spend; the accumulator stays usable afterwards. */
210
312
  finish(): DeepSeekTodaySpend;
211
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
+ }
212
404
  /**
213
405
  * Merge one priced event's contribution into an accumulator spend (pure:
214
406
  * returns a new spend, never mutates its input).
@@ -226,7 +418,11 @@ export declare function addEventContribution(spend: DeepSeekTodaySpend, priced:
226
418
  */
227
419
  export declare function mergeTodaySpend(target: DeepSeekTodaySpend, source: DeepSeekTodaySpend): DeepSeekTodaySpend;
228
420
  /**
229
- * Price one session's complete event log at the official per-model rates.
421
+ * Price one session's complete event log at the official per-model rates,
422
+ * with DSH's attempt semantics: every provider-reported sample (an
423
+ * `assistant/message`'s usage, or an `assistant/attempt`'s stream usage)
424
+ * contributes, a later sample for the same `(turn, step)` replaces the earlier
425
+ * one, and `llm/retry-started` makes the retried attempt add.
230
426
  * @param events - one session's complete event log.
231
427
  * @param billing - resolved pricing with peak-hour windows.
232
428
  * @param catalog - model display rows, in presentation order.
@@ -241,13 +437,13 @@ export declare function computeSessionSpend(events: readonly SessionEvent[], bil
241
437
  name: string;
242
438
  }[], startSeq?: number): DeepSeekSessionSpend;
243
439
  /**
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.
440
+ * Price one completed Turn's billed usage, identified by its closing
441
+ * assistant message id. The turn's events are those between its `turn/start`
442
+ * and `turn/end` (both matched by the message's own turn coordinate), priced
443
+ * with the same attempt semantics as {@link computeSessionSpend}. A message
444
+ * that cannot be located, a turn without bracketing `turn/start` / `turn/end`
445
+ * events (for example after compaction), or a session with no priced usage
446
+ * prices to zero.
251
447
  * @param events - one session's complete event log.
252
448
  * @param billing - resolved pricing with peak-hour windows.
253
449
  * @param catalog - model display rows, in presentation order.
@@ -259,10 +455,70 @@ export declare function computeTurnSpend(events: readonly SessionEvent[], billin
259
455
  name: string;
260
456
  }[], messageId: string): DeepSeekTurnSpend;
261
457
  /**
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.
458
+ * Incremental single-pass fold of one session's completed-Turn costs, keyed by
459
+ * the id of every assistant message inside each Turn. Feeding the fold only
460
+ * the appended tail keeps a growing session's map current in O(new events)
461
+ * instead of re-scanning the whole log per message.
462
+ *
463
+ * Semantics are exactly {@link computeTurnSpend}'s: a Turn is the
464
+ * `turn/start`..`turn/end` range (matched by the event's own turn coordinate),
465
+ * every priced event inside it contributes at its own timestamp's rate, and a
466
+ * message outside any bracket contributes nothing.
467
+ */
468
+ export declare class SessionTurnSpendFolder {
469
+ private readonly billing;
470
+ private readonly catalog;
471
+ private readonly rows;
472
+ private ids;
473
+ /** Events of the open Turn, folded with the shared attempt semantics on close. */
474
+ private events;
475
+ private open;
476
+ /** Events already fed; a shorter log resets the fold. */
477
+ private cursor;
478
+ /**
479
+ * @param billing - resolved pricing with peak-hour windows.
480
+ * @param catalog - model display rows, in presentation order.
481
+ */
482
+ constructor(billing: ResolvedBilling, catalog: readonly {
483
+ id: string;
484
+ name: string;
485
+ }[]);
486
+ /** How many events have been folded so far (the host's incremental cursor). */
487
+ get processed(): number;
488
+ /**
489
+ * Fold every event from the cursor to the end of the log. A log shorter than
490
+ * the cursor (rewritten session) restarts the fold from an empty state.
491
+ * @param events - the session's complete event log, in seq order.
492
+ */
493
+ feed(events: readonly SessionEvent[]): void;
494
+ /** The folded map; the fold stays usable afterwards. */
495
+ finish(): DeepSeekSessionTurnSpends;
496
+ /** Drop the fold state so the next feed starts from the log's beginning. */
497
+ private reset;
498
+ }
499
+ /**
500
+ * Price every completed Turn of one session in a single pass (the pure
501
+ * equivalent of {@link SessionTurnSpendFolder}).
502
+ * @param events - one session's complete event log.
503
+ * @param billing - resolved pricing with peak-hour windows.
504
+ * @param catalog - model display rows, in presentation order.
505
+ * @returns one row per assistant message inside a completed Turn, in log order.
506
+ */
507
+ export declare function computeSessionTurnSpends(events: readonly SessionEvent[], billing: ResolvedBilling, catalog: readonly {
508
+ id: string;
509
+ name: string;
510
+ }[]): DeepSeekSessionTurnSpends;
511
+ /**
512
+ * Price one session's log for the Beijing-time calendar day of `now`. Events
513
+ * after the reference day are ignored; the fold's latest-day state then
514
+ * answers the query exactly (empty when the session's latest priced day is not
515
+ * the reference day). Pricing follows {@link applyBillingEvent} (attempt
516
+ * samples with same-step replacement).
517
+ *
518
+ * The fold's `(turn, step)` replacement slot is per session, so callers must
519
+ * pass ONE session's log; aggregate across sessions with
520
+ * {@link mergeTodaySpend}.
521
+ * @param events - one session's complete event log.
266
522
  * @param billing - resolved pricing with peak-hour windows.
267
523
  * @param catalog - model display rows, in presentation order.
268
524
  * @param now - the reference moment whose Beijing-time calendar day is "today".