@rayadesu/dsh-llm-billing 0.2.2 → 0.2.4
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 +1 -1
- package/README.zh.md +1 -1
- package/lib/index.js +450 -117
- package/lib/typert.host.js +77 -3
- package/lib/typert.remote-client.d.ts +5 -1
- package/lib/typert.remote-client.js +77 -3
- package/lib/types/balance.d.ts +31 -1
- package/lib/types/balance.js +29 -0
- package/lib/types/billing.d.ts +41 -6
- package/lib/types/billing.js +132 -70
- package/lib/types/index.d.ts +7 -2
- package/lib/types/index.js +37 -5
- package/lib/types/today-spend.d.ts +62 -10
- package/lib/types/today-spend.js +252 -54
- package/lib/types/types.d.ts +23 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -135,12 +135,16 @@ let DeepSeekBalanceGateway = (() => {
|
|
|
135
135
|
let _getBalance_decorators;
|
|
136
136
|
let _getSessionSpend_decorators;
|
|
137
137
|
let _getTodaySpend_decorators;
|
|
138
|
+
let _getTodaySessionsSpend_decorators;
|
|
139
|
+
let _getTurnSpend_decorators;
|
|
138
140
|
return class DeepSeekBalanceGateway extends _classSuper {
|
|
139
141
|
static {
|
|
140
142
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
141
143
|
_getBalance_decorators = [Remote("getBalance")];
|
|
142
144
|
_getSessionSpend_decorators = [Remote("getSessionSpend")];
|
|
143
145
|
_getTodaySpend_decorators = [Remote("getTodaySpend")];
|
|
146
|
+
_getTodaySessionsSpend_decorators = [Remote("getTodaySessionsSpend")];
|
|
147
|
+
_getTurnSpend_decorators = [Remote("getTurnSpend")];
|
|
144
148
|
__esDecorate(this, null, _getBalance_decorators, {
|
|
145
149
|
kind: "method",
|
|
146
150
|
name: "getBalance",
|
|
@@ -174,6 +178,28 @@ let DeepSeekBalanceGateway = (() => {
|
|
|
174
178
|
},
|
|
175
179
|
metadata: _metadata
|
|
176
180
|
}, null, _instanceExtraInitializers);
|
|
181
|
+
__esDecorate(this, null, _getTodaySessionsSpend_decorators, {
|
|
182
|
+
kind: "method",
|
|
183
|
+
name: "getTodaySessionsSpend",
|
|
184
|
+
static: false,
|
|
185
|
+
private: false,
|
|
186
|
+
access: {
|
|
187
|
+
has: (obj) => "getTodaySessionsSpend" in obj,
|
|
188
|
+
get: (obj) => obj.getTodaySessionsSpend
|
|
189
|
+
},
|
|
190
|
+
metadata: _metadata
|
|
191
|
+
}, null, _instanceExtraInitializers);
|
|
192
|
+
__esDecorate(this, null, _getTurnSpend_decorators, {
|
|
193
|
+
kind: "method",
|
|
194
|
+
name: "getTurnSpend",
|
|
195
|
+
static: false,
|
|
196
|
+
private: false,
|
|
197
|
+
access: {
|
|
198
|
+
has: (obj) => "getTurnSpend" in obj,
|
|
199
|
+
get: (obj) => obj.getTurnSpend
|
|
200
|
+
},
|
|
201
|
+
metadata: _metadata
|
|
202
|
+
}, null, _instanceExtraInitializers);
|
|
177
203
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
178
204
|
enumerable: true,
|
|
179
205
|
configurable: true,
|
|
@@ -218,6 +244,29 @@ let DeepSeekBalanceGateway = (() => {
|
|
|
218
244
|
getTodaySpend(force) {
|
|
219
245
|
return this.options.fetchTodaySpend(force ?? false);
|
|
220
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Read today's billed spend per session, priced per event by its
|
|
249
|
+
* Beijing-time calendar day, hour, and weekday (weekends are always
|
|
250
|
+
* off-peak). Rows carry the session's durable title and sort by cost
|
|
251
|
+
* descending; sessions with no priced usage on the day are omitted.
|
|
252
|
+
* @param force - bypass the host-side 60s cache (manual refresh); omitted
|
|
253
|
+
* means a cached read.
|
|
254
|
+
* @returns today's per-session rows, highest first.
|
|
255
|
+
*/
|
|
256
|
+
getTodaySessionsSpend(force) {
|
|
257
|
+
return this.options.fetchTodaySessionsSpend(force ?? false);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Read one completed Turn's billed spend, priced per event by its
|
|
261
|
+
* Beijing-time hour and weekday (weekends are always off-peak).
|
|
262
|
+
* @param sessionId - the session owning the Turn.
|
|
263
|
+
* @param messageId - the closing assistant message's durable id, which
|
|
264
|
+
* locates the Turn in the session log.
|
|
265
|
+
* @returns the Turn's total cost in CNY.
|
|
266
|
+
*/
|
|
267
|
+
getTurnSpend(sessionId, messageId) {
|
|
268
|
+
return this.options.fetchTurnSpend(sessionId, messageId);
|
|
269
|
+
}
|
|
221
270
|
};
|
|
222
271
|
})();
|
|
223
272
|
//#endregion
|
|
@@ -228,11 +277,12 @@ let DeepSeekBalanceGateway = (() => {
|
|
|
228
277
|
* Remote gateway stays transport-free and the whole spend is testable without
|
|
229
278
|
* a key.
|
|
230
279
|
*
|
|
231
|
-
* The per-event pricing lives in {@link priceEvent}
|
|
232
|
-
*
|
|
233
|
-
* {@link computeTodaySpend})
|
|
234
|
-
*
|
|
235
|
-
* pricing-table change cannot drift one path
|
|
280
|
+
* The per-event pricing lives in {@link priceEvent} and the fold in the
|
|
281
|
+
* {@link SpendAccumulator}: the events-scan paths ({@link computeSessionSpend},
|
|
282
|
+
* {@link computeTodaySpend}), the session-projection unit (`billingTodaySpend`
|
|
283
|
+
* in projection.ts), and the scanner's single-pass events path all price
|
|
284
|
+
* through the same primitives, so a pricing-table change cannot drift one path
|
|
285
|
+
* from the others.
|
|
236
286
|
* @module @rayadesu/dsh-llm-billing/billing
|
|
237
287
|
*/
|
|
238
288
|
/**
|
|
@@ -311,20 +361,28 @@ function resolveBilling(config) {
|
|
|
311
361
|
models
|
|
312
362
|
};
|
|
313
363
|
}
|
|
314
|
-
/** The Beijing (Asia/Shanghai, UTC+8, no DST) hour of a timestamp. */
|
|
315
|
-
function beijingHour(now) {
|
|
316
|
-
return new Date(now.getTime() + 288e5).getUTCHours();
|
|
317
|
-
}
|
|
318
364
|
/**
|
|
319
|
-
*
|
|
320
|
-
*
|
|
365
|
+
* Derive the Beijing hour, weekday, and calendar-day key of one timestamp from
|
|
366
|
+
* a single shifted `Date` — every timezone-sensitive read shares this one
|
|
367
|
+
* implementation, so the pieces cannot drift apart.
|
|
368
|
+
* @param time - epoch milliseconds.
|
|
321
369
|
*/
|
|
322
|
-
function
|
|
323
|
-
|
|
370
|
+
function beijingParts(time) {
|
|
371
|
+
const shifted = new Date(time + 288e5);
|
|
372
|
+
return {
|
|
373
|
+
hour: shifted.getUTCHours(),
|
|
374
|
+
weekday: shifted.getUTCDay(),
|
|
375
|
+
dayKey: shifted.toISOString().slice(0, 10)
|
|
376
|
+
};
|
|
324
377
|
}
|
|
325
378
|
/** The Beijing (Asia/Shanghai, UTC+8, no DST) calendar-day key of a timestamp. */
|
|
326
379
|
function beijingDayKey(now) {
|
|
327
|
-
return
|
|
380
|
+
return beijingParts(now.getTime()).dayKey;
|
|
381
|
+
}
|
|
382
|
+
/** Whether a Beijing (hour, weekday) pair falls inside any peak-hour window. */
|
|
383
|
+
function isPeakParts(billing, hour, weekday) {
|
|
384
|
+
if (weekday === 0 || weekday === 6) return false;
|
|
385
|
+
return billing.peakHours.some(({ start, end }) => hour >= start && hour < end);
|
|
328
386
|
}
|
|
329
387
|
/**
|
|
330
388
|
* Whether a timestamp falls inside any peak-hour window (Beijing time,
|
|
@@ -335,10 +393,8 @@ function beijingDayKey(now) {
|
|
|
335
393
|
* @returns true during a weekday peak hour.
|
|
336
394
|
*/
|
|
337
395
|
function isPeak(billing, now) {
|
|
338
|
-
const weekday =
|
|
339
|
-
|
|
340
|
-
const hour = beijingHour(now);
|
|
341
|
-
return billing.peakHours.some(({ start, end }) => hour >= start && hour < end);
|
|
396
|
+
const { hour, weekday } = beijingParts(now.getTime());
|
|
397
|
+
return isPeakParts(billing, hour, weekday);
|
|
342
398
|
}
|
|
343
399
|
/**
|
|
344
400
|
* Price one event at the official per-model rates, applying the peak/off-peak
|
|
@@ -360,8 +416,8 @@ function priceEvent(event, billing, names) {
|
|
|
360
416
|
const model = event.data.message.source.model;
|
|
361
417
|
const pricing = billing.models.get(model);
|
|
362
418
|
if (pricing === void 0) return void 0;
|
|
363
|
-
const
|
|
364
|
-
const peak =
|
|
419
|
+
const { hour, weekday, dayKey } = beijingParts(event.time);
|
|
420
|
+
const peak = isPeakParts(billing, hour, weekday);
|
|
365
421
|
const price = peak ? pricing.peak : pricing.offPeak;
|
|
366
422
|
const hit = reported.cacheReadTokens ?? 0;
|
|
367
423
|
const miss = reported.inputTokens + (reported.cacheWriteTokens ?? 0);
|
|
@@ -371,7 +427,7 @@ function priceEvent(event, billing, names) {
|
|
|
371
427
|
const outputCost = output * price.output / 1e6;
|
|
372
428
|
const cost = hitCost + missCost + outputCost;
|
|
373
429
|
return {
|
|
374
|
-
dayKey
|
|
430
|
+
dayKey,
|
|
375
431
|
model,
|
|
376
432
|
displayName: names.get(model) ?? model,
|
|
377
433
|
cost,
|
|
@@ -408,6 +464,48 @@ function contributionModel(priced) {
|
|
|
408
464
|
outputCost: priced.outputCost
|
|
409
465
|
};
|
|
410
466
|
}
|
|
467
|
+
/** Sum two model rows of the same model (pure). */
|
|
468
|
+
function mergeModelRows(left, right) {
|
|
469
|
+
return {
|
|
470
|
+
model: left.model,
|
|
471
|
+
displayName: left.displayName,
|
|
472
|
+
cost: left.cost + right.cost,
|
|
473
|
+
peakCost: left.peakCost + right.peakCost,
|
|
474
|
+
offPeakCost: left.offPeakCost + right.offPeakCost,
|
|
475
|
+
cacheHitInputTokens: left.cacheHitInputTokens + right.cacheHitInputTokens,
|
|
476
|
+
cacheMissInputTokens: left.cacheMissInputTokens + right.cacheMissInputTokens,
|
|
477
|
+
outputTokens: left.outputTokens + right.outputTokens,
|
|
478
|
+
cacheHitInputCost: left.cacheHitInputCost + right.cacheHitInputCost,
|
|
479
|
+
cacheMissInputCost: left.cacheMissInputCost + right.cacheMissInputCost,
|
|
480
|
+
outputCost: left.outputCost + right.outputCost
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Mutable model-row accumulator behind every spend fold. Rows keep first-seen
|
|
485
|
+
* model order — the same shape a pure `addEventContribution` chain produces —
|
|
486
|
+
* so the single-pass scan path and the pure public paths cannot diverge. One
|
|
487
|
+
* `Map` lookup per contribution instead of a per-event array copy: the huge
|
|
488
|
+
* event-log folds allocate one row object per model, not one intermediate
|
|
489
|
+
* array per event.
|
|
490
|
+
*/
|
|
491
|
+
var SpendAccumulator = class {
|
|
492
|
+
rows = /* @__PURE__ */ new Map();
|
|
493
|
+
total = 0;
|
|
494
|
+
/** Add one priced contribution. */
|
|
495
|
+
add(priced) {
|
|
496
|
+
const row = contributionModel(priced);
|
|
497
|
+
const existing = this.rows.get(priced.model);
|
|
498
|
+
this.rows.set(priced.model, existing === void 0 ? row : mergeModelRows(existing, row));
|
|
499
|
+
this.total += priced.cost;
|
|
500
|
+
}
|
|
501
|
+
/** The folded spend; the accumulator stays usable afterwards. */
|
|
502
|
+
finish() {
|
|
503
|
+
return {
|
|
504
|
+
total: this.total,
|
|
505
|
+
models: [...this.rows.values()]
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
};
|
|
411
509
|
/**
|
|
412
510
|
* Merge one priced event's contribution into an accumulator spend (pure:
|
|
413
511
|
* returns a new spend, never mutates its input).
|
|
@@ -416,19 +514,9 @@ function contributionModel(priced) {
|
|
|
416
514
|
* @returns the merged spend.
|
|
417
515
|
*/
|
|
418
516
|
function addEventContribution(spend, priced) {
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
peakCost: row.peakCost + priced.peakCost,
|
|
423
|
-
offPeakCost: row.offPeakCost + priced.offPeakCost,
|
|
424
|
-
cacheHitInputTokens: row.cacheHitInputTokens + priced.cacheHitInputTokens,
|
|
425
|
-
cacheMissInputTokens: row.cacheMissInputTokens + priced.cacheMissInputTokens,
|
|
426
|
-
outputTokens: row.outputTokens + priced.outputTokens,
|
|
427
|
-
cacheHitInputCost: row.cacheHitInputCost + priced.cacheHitInputCost,
|
|
428
|
-
cacheMissInputCost: row.cacheMissInputCost + priced.cacheMissInputCost,
|
|
429
|
-
outputCost: row.outputCost + priced.outputCost
|
|
430
|
-
} : row);
|
|
431
|
-
if (!rows.some((row) => row.model === priced.model)) rows.push(contributionModel(priced));
|
|
517
|
+
const row = contributionModel(priced);
|
|
518
|
+
const rows = spend.models.map((existing) => existing.model === priced.model ? mergeModelRows(existing, row) : existing);
|
|
519
|
+
if (!rows.some((existing) => existing.model === priced.model)) rows.push(row);
|
|
432
520
|
return {
|
|
433
521
|
total: spend.total + priced.cost,
|
|
434
522
|
models: rows
|
|
@@ -442,22 +530,16 @@ function addEventContribution(spend, priced) {
|
|
|
442
530
|
* @returns the summed spend.
|
|
443
531
|
*/
|
|
444
532
|
function mergeTodaySpend(target, source) {
|
|
445
|
-
|
|
446
|
-
for (const row of
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
outputTokens: row.outputTokens,
|
|
456
|
-
cacheHitInputCost: row.cacheHitInputCost,
|
|
457
|
-
cacheMissInputCost: row.cacheMissInputCost,
|
|
458
|
-
outputCost: row.outputCost
|
|
459
|
-
});
|
|
460
|
-
return merged;
|
|
533
|
+
const rows = /* @__PURE__ */ new Map();
|
|
534
|
+
for (const row of target.models) rows.set(row.model, row);
|
|
535
|
+
for (const row of source.models) {
|
|
536
|
+
const existing = rows.get(row.model);
|
|
537
|
+
rows.set(row.model, existing === void 0 ? row : mergeModelRows(existing, row));
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
total: target.total + source.total,
|
|
541
|
+
models: [...rows.values()]
|
|
542
|
+
};
|
|
461
543
|
}
|
|
462
544
|
/**
|
|
463
545
|
* Price a set of billed events at the official per-model rates, applying the
|
|
@@ -470,21 +552,18 @@ function mergeTodaySpend(target, source) {
|
|
|
470
552
|
* published table prices only the two V4 rows).
|
|
471
553
|
* @param events - the events to price.
|
|
472
554
|
* @param billing - resolved pricing with peak-hour windows.
|
|
473
|
-
* @param
|
|
555
|
+
* @param names - model id → display label.
|
|
556
|
+
* @param dayKey - when provided, only events on this Beijing calendar day contribute.
|
|
474
557
|
* @returns the total cost plus one row per priced model.
|
|
475
558
|
*/
|
|
476
|
-
function priceEvents(events, billing,
|
|
477
|
-
const
|
|
478
|
-
let spend = {
|
|
479
|
-
total: 0,
|
|
480
|
-
models: []
|
|
481
|
-
};
|
|
559
|
+
function priceEvents(events, billing, names, dayKey) {
|
|
560
|
+
const accumulator = new SpendAccumulator();
|
|
482
561
|
for (const event of events) {
|
|
483
562
|
const priced = priceEvent(event, billing, names);
|
|
484
|
-
if (priced === void 0) continue;
|
|
485
|
-
|
|
563
|
+
if (priced === void 0 || dayKey !== void 0 && priced.dayKey !== dayKey) continue;
|
|
564
|
+
accumulator.add(priced);
|
|
486
565
|
}
|
|
487
|
-
return
|
|
566
|
+
return accumulator.finish();
|
|
488
567
|
}
|
|
489
568
|
/**
|
|
490
569
|
* Price one session's complete event log at the official per-model rates.
|
|
@@ -494,7 +573,45 @@ function priceEvents(events, billing, catalog) {
|
|
|
494
573
|
* @returns the session's total cost plus one row per priced model.
|
|
495
574
|
*/
|
|
496
575
|
function computeSessionSpend(events, billing, catalog) {
|
|
497
|
-
return priceEvents(events, billing, catalog);
|
|
576
|
+
return priceEvents(events, billing, new Map(catalog.map((model) => [model.id, model.name])));
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Price one completed Turn's billed usage at the official per-model rates,
|
|
580
|
+
* identified by its closing assistant message id. The turn's events are those
|
|
581
|
+
* between its `turn/start` and `turn/end` (both matched by the message's own
|
|
582
|
+
* turn coordinate); each priced event applies the peak/off-peak table by its
|
|
583
|
+
* Beijing-time hour and weekday. A message that cannot be located, a turn
|
|
584
|
+
* without bracketing `turn/start` / `turn/end` events (for example after
|
|
585
|
+
* compaction), or a session with no priced usage prices to zero.
|
|
586
|
+
* @param events - one session's complete event log.
|
|
587
|
+
* @param billing - resolved pricing with peak-hour windows.
|
|
588
|
+
* @param catalog - model display rows, in presentation order.
|
|
589
|
+
* @param messageId - the closing assistant message's durable id.
|
|
590
|
+
* @returns the turn's total cost in CNY.
|
|
591
|
+
*/
|
|
592
|
+
function computeTurnSpend(events, billing, catalog, messageId) {
|
|
593
|
+
const names = new Map(catalog.map((model) => [model.id, model.name]));
|
|
594
|
+
let turn;
|
|
595
|
+
for (const event of events) {
|
|
596
|
+
if (event.type !== "assistant/message") continue;
|
|
597
|
+
if (event.data.message.id !== messageId) continue;
|
|
598
|
+
turn = event.data.turn;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
if (turn === void 0) return { total: 0 };
|
|
602
|
+
const accumulator = new SpendAccumulator();
|
|
603
|
+
let active = false;
|
|
604
|
+
for (const event of events) {
|
|
605
|
+
if (event.type === "turn/start" && event.data.turn === turn) {
|
|
606
|
+
active = true;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (event.type === "turn/end" && event.data.turn === turn) break;
|
|
610
|
+
if (!active) continue;
|
|
611
|
+
const priced = priceEvent(event, billing, names);
|
|
612
|
+
if (priced !== void 0) accumulator.add(priced);
|
|
613
|
+
}
|
|
614
|
+
return { total: accumulator.finish().total };
|
|
498
615
|
}
|
|
499
616
|
/**
|
|
500
617
|
* Price every event whose Beijing-time calendar day is the day of `now`,
|
|
@@ -508,14 +625,7 @@ function computeSessionSpend(events, billing, catalog) {
|
|
|
508
625
|
*/
|
|
509
626
|
function computeTodaySpend(events, billing, catalog, now = /* @__PURE__ */ new Date()) {
|
|
510
627
|
const day = beijingDayKey(now);
|
|
511
|
-
|
|
512
|
-
let spend = emptyTodaySpend();
|
|
513
|
-
for (const event of events) {
|
|
514
|
-
const priced = priceEvent(event, billing, names);
|
|
515
|
-
if (priced === void 0 || priced.dayKey !== day) continue;
|
|
516
|
-
spend = addEventContribution(spend, priced);
|
|
517
|
-
}
|
|
518
|
-
return spend;
|
|
628
|
+
return priceEvents(events, billing, new Map(catalog.map((model) => [model.id, model.name])), day);
|
|
519
629
|
}
|
|
520
630
|
//#endregion
|
|
521
631
|
//#region lib/types/projection.js
|
|
@@ -618,9 +728,10 @@ function foldBillingUnit(unit, events) {
|
|
|
618
728
|
* with write-back) or, without the cache service, one detached local fold
|
|
619
729
|
* over a full `inspect`. Persisted revisions gate every cold read, so a
|
|
620
730
|
* session whose log did not change since the last resolution costs nothing.
|
|
621
|
-
* - events path (plans A2/A3): collect only today's events
|
|
622
|
-
* Beijing-day filter during collection) with a hard cap,
|
|
623
|
-
* whose persisted revision is unchanged since the last
|
|
731
|
+
* - events path (plans A2/A3): collect and price only today's events in one
|
|
732
|
+
* pass (per-event Beijing-day filter during collection) with a hard cap,
|
|
733
|
+
* skipping sessions whose persisted revision is unchanged since the last
|
|
734
|
+
* scan.
|
|
624
735
|
*
|
|
625
736
|
* Both strategies run behind the same {@link TodaySpendCache}, so a miss
|
|
626
737
|
* happens at most once per 60 seconds per process, and a manual refresh
|
|
@@ -628,11 +739,36 @@ function foldBillingUnit(unit, events) {
|
|
|
628
739
|
* unchanged log provably cannot change the aggregate.
|
|
629
740
|
* @module @rayadesu/dsh-llm-billing/today-spend
|
|
630
741
|
*/
|
|
631
|
-
/**
|
|
742
|
+
/**
|
|
743
|
+
* Fold one session's durable display title: the latest `session/title`
|
|
744
|
+
* event's text (last-wins, matching the `title` projection), or `null` before
|
|
745
|
+
* the first title lands. The fold runs over the complete log, so an explicit
|
|
746
|
+
* user rename is picked up as soon as its event commits.
|
|
747
|
+
* @param events - one session's complete event log.
|
|
748
|
+
* @returns the session's current title, or `null` when untitled.
|
|
749
|
+
*/
|
|
750
|
+
function foldSessionTitle(events) {
|
|
751
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
752
|
+
const event = events[index];
|
|
753
|
+
if (event.type !== "session/title") continue;
|
|
754
|
+
const data = event.data;
|
|
755
|
+
return typeof data.title === "string" ? data.title : null;
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Bounded parallel fan-out: run `run` over `items` with at most `limit` in
|
|
761
|
+
* flight. A shared index counter hands each worker its next job, so the
|
|
762
|
+
* dispatch is O(n) overall (array `shift()` would be O(n) per pop).
|
|
763
|
+
*/
|
|
632
764
|
async function withConcurrency(items, limit, run) {
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
765
|
+
const total = items.length;
|
|
766
|
+
let next = 0;
|
|
767
|
+
await Promise.all(Array.from({ length: Math.min(limit, total) }, async () => {
|
|
768
|
+
for (let job = next; job < total; job = next) {
|
|
769
|
+
next += 1;
|
|
770
|
+
await run(items[job]);
|
|
771
|
+
}
|
|
636
772
|
}));
|
|
637
773
|
}
|
|
638
774
|
/**
|
|
@@ -640,6 +776,8 @@ async function withConcurrency(items, limit, run) {
|
|
|
640
776
|
* coalesces concurrent misses, and a `force` bypass for the manual refresh
|
|
641
777
|
* path. Cross-day invalidation is automatic (the day key changes); a failed
|
|
642
778
|
* scan leaves the previous value in place and retries on the next call.
|
|
779
|
+
* @typeParam T - the cached aggregate's value shape (the spend or its
|
|
780
|
+
* per-session breakdown).
|
|
643
781
|
*/
|
|
644
782
|
var TodaySpendCache = class {
|
|
645
783
|
scan;
|
|
@@ -650,9 +788,9 @@ var TodaySpendCache = class {
|
|
|
650
788
|
cachedAt = 0;
|
|
651
789
|
inFlight;
|
|
652
790
|
/**
|
|
791
|
+
* @param scan - the aggregate computation behind a miss.
|
|
653
792
|
* @param ttlMs - time window in milliseconds (default 60 000).
|
|
654
793
|
* @param now - clock source (injectable for tests).
|
|
655
|
-
* @param scan - the aggregate computation behind a miss.
|
|
656
794
|
*/
|
|
657
795
|
constructor(scan, ttlMs = 6e4, now = () => /* @__PURE__ */ new Date()) {
|
|
658
796
|
this.scan = scan;
|
|
@@ -693,7 +831,7 @@ var TodaySpendCache = class {
|
|
|
693
831
|
*/
|
|
694
832
|
var TodaySpendScanner = class {
|
|
695
833
|
deps;
|
|
696
|
-
/** Cold sessions resolved on the projection path: id → revision + unit state. */
|
|
834
|
+
/** Cold sessions resolved on the projection path: id → revision + unit state + title. */
|
|
697
835
|
coldResolved = /* @__PURE__ */ new Map();
|
|
698
836
|
/** Cold sessions resolved on the events path: id → revision (events were collected). */
|
|
699
837
|
lastEventsScan;
|
|
@@ -710,16 +848,63 @@ var TodaySpendScanner = class {
|
|
|
710
848
|
this.deps.ensureUnit?.();
|
|
711
849
|
return this.scanProjections(dayKey);
|
|
712
850
|
}
|
|
851
|
+
/**
|
|
852
|
+
* Compute today's per-session spend for one Beijing day, sorted by cost
|
|
853
|
+
* descending. Sessions with no priced usage on the day are omitted; each
|
|
854
|
+
* row carries the session's durable title folded from its log.
|
|
855
|
+
* @param dayKey - the Beijing-time calendar-day key to aggregate.
|
|
856
|
+
* @returns today's per-session rows, highest first.
|
|
857
|
+
*/
|
|
858
|
+
async scanSessions(dayKey) {
|
|
859
|
+
const rows = this.deps.projections?.() === void 0 ? await this.scanSessionsEvents(dayKey) : await this.scanSessionsProjections(dayKey);
|
|
860
|
+
rows.sort((left, right) => right.total - left.total);
|
|
861
|
+
return { sessions: rows };
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Resolve one cold session's billing unit state and display title through
|
|
865
|
+
* the projection-cache ladder (cached row first, then a detached local
|
|
866
|
+
* fold over a full inspect). A cache-served value carries no title (the
|
|
867
|
+
* ladder only stores projection values), so such rows report `title: null`
|
|
868
|
+
* until the session is inspected again.
|
|
869
|
+
* @param id - the cold session's id.
|
|
870
|
+
* @returns the resolved state and title, or `undefined` when unreadable.
|
|
871
|
+
*/
|
|
872
|
+
async resolveCold(id) {
|
|
873
|
+
const { persistence, projectionCache, unit, logger } = this.deps;
|
|
874
|
+
const cache = projectionCache?.();
|
|
875
|
+
if (cache !== void 0) try {
|
|
876
|
+
const value = (await cache.coldSnapshot(id)).values[BILLING_UNIT_KEY];
|
|
877
|
+
if (value !== void 0) return {
|
|
878
|
+
value,
|
|
879
|
+
title: null
|
|
880
|
+
};
|
|
881
|
+
} catch (error) {
|
|
882
|
+
logger.warn(`llm-billing: projection cold read for session ${id} failed: ${String(error)}`);
|
|
883
|
+
}
|
|
884
|
+
const persistenceService = persistence?.();
|
|
885
|
+
if (persistenceService === void 0) return void 0;
|
|
886
|
+
try {
|
|
887
|
+
const inspection = await persistenceService.inspect(id);
|
|
888
|
+
return {
|
|
889
|
+
value: foldBillingUnit(unit, inspection.events),
|
|
890
|
+
title: foldSessionTitle(inspection.events)
|
|
891
|
+
};
|
|
892
|
+
} catch (error) {
|
|
893
|
+
logger.warn(`llm-billing: skipping unreadable session ${id}: ${String(error)}`);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
713
897
|
/** Projection path: eager cells for live sessions, revision-gated cold ladder for the rest. */
|
|
714
898
|
async scanProjections(dayKey) {
|
|
715
|
-
const { sessions, persistence, projections
|
|
899
|
+
const { sessions, persistence, projections } = this.deps;
|
|
900
|
+
const projectionsService = projections?.();
|
|
716
901
|
let total = emptyTodaySpend();
|
|
717
902
|
const liveIds = /* @__PURE__ */ new Set();
|
|
718
903
|
if (sessions !== void 0) {
|
|
719
904
|
const store = sessions();
|
|
720
905
|
if (store !== void 0) for (const session of store.list()) {
|
|
721
906
|
liveIds.add(session.id);
|
|
722
|
-
const state =
|
|
907
|
+
const state = projectionsService?.stateOf(session, BILLING_UNIT_KEY);
|
|
723
908
|
if (state !== void 0 && state.dayKey === dayKey) total = mergeTodaySpend(total, state.spend);
|
|
724
909
|
}
|
|
725
910
|
}
|
|
@@ -740,22 +925,10 @@ var TodaySpendScanner = class {
|
|
|
740
925
|
});
|
|
741
926
|
}
|
|
742
927
|
await withConcurrency(pending, 8, async ({ id, revision }) => {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (cache !== void 0) try {
|
|
746
|
-
value = (await cache.coldSnapshot(id)).values[BILLING_UNIT_KEY];
|
|
747
|
-
} catch (error) {
|
|
748
|
-
logger.warn(`llm-billing: projection cold read for session ${id} failed: ${String(error)}`);
|
|
749
|
-
}
|
|
750
|
-
if (value === void 0) try {
|
|
751
|
-
const inspection = await persistenceService.inspect(id);
|
|
752
|
-
value = foldBillingUnit(unit, inspection.events);
|
|
753
|
-
} catch (error) {
|
|
754
|
-
logger.warn(`llm-billing: skipping unreadable session ${id}: ${String(error)}`);
|
|
755
|
-
}
|
|
756
|
-
if (value !== void 0) this.coldResolved.set(id, {
|
|
928
|
+
const resolved = await this.resolveCold(id);
|
|
929
|
+
if (resolved !== void 0) this.coldResolved.set(id, {
|
|
757
930
|
revision,
|
|
758
|
-
|
|
931
|
+
...resolved
|
|
759
932
|
});
|
|
760
933
|
});
|
|
761
934
|
for (const { id } of pending) {
|
|
@@ -764,24 +937,158 @@ var TodaySpendScanner = class {
|
|
|
764
937
|
}
|
|
765
938
|
return total;
|
|
766
939
|
}
|
|
767
|
-
/**
|
|
940
|
+
/**
|
|
941
|
+
* Events path: price today's events in a single pass (per-event Beijing-day
|
|
942
|
+
* filter during collection, hard cap), gated by revisions.
|
|
943
|
+
*/
|
|
768
944
|
async scanEvents(dayKey) {
|
|
769
945
|
const { sessions, persistence, maxEvents, logger, billing, catalog } = this.deps;
|
|
770
|
-
const
|
|
946
|
+
const names = new Map(catalog.map((model) => [model.id, model.name]));
|
|
947
|
+
const accumulator = new SpendAccumulator();
|
|
771
948
|
const liveIds = /* @__PURE__ */ new Set();
|
|
949
|
+
let collected = 0;
|
|
772
950
|
let truncated = false;
|
|
951
|
+
const collect = (events) => {
|
|
952
|
+
for (const event of events) {
|
|
953
|
+
if (beijingDayKey(new Date(event.time)) !== dayKey) continue;
|
|
954
|
+
collected += 1;
|
|
955
|
+
if (collected > maxEvents) {
|
|
956
|
+
truncated = true;
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const priced = priceEvent(event, billing, names);
|
|
960
|
+
if (priced !== void 0) accumulator.add(priced);
|
|
961
|
+
}
|
|
962
|
+
};
|
|
773
963
|
if (sessions !== void 0) {
|
|
774
964
|
const store = sessions();
|
|
775
965
|
if (store !== void 0) for (const session of store.list()) {
|
|
776
966
|
liveIds.add(session.id);
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
967
|
+
collect(session.events);
|
|
968
|
+
if (truncated) break;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const persistenceService = persistence?.();
|
|
972
|
+
if (!truncated && persistenceService !== void 0) {
|
|
973
|
+
const snapshots = await persistenceService.listSnapshots();
|
|
974
|
+
for (const { header, revision } of snapshots) {
|
|
975
|
+
if (liveIds.has(header.id)) continue;
|
|
976
|
+
if (this.lastEventsScan?.get(header.id) === revision) continue;
|
|
977
|
+
try {
|
|
978
|
+
collect((await persistenceService.inspect(header.id)).events);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
|
|
981
|
+
}
|
|
982
|
+
if (truncated) break;
|
|
983
|
+
}
|
|
984
|
+
if (!truncated) this.lastEventsScan = new Map(snapshots.map((snapshot) => [snapshot.header.id, snapshot.revision]));
|
|
985
|
+
}
|
|
986
|
+
if (truncated) logger.warn(`llm-billing: today's events exceeded ${maxEvents}; result truncated`);
|
|
987
|
+
return accumulator.finish();
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Projection-path per-session scan: eager cells for live sessions (title
|
|
991
|
+
* folded from the live log, so a rename is reflected immediately),
|
|
992
|
+
* revision-gated cold ladder for the rest (title resolved on inspect,
|
|
993
|
+
* `null` when served from the projection cache).
|
|
994
|
+
* @param dayKey - the Beijing-time calendar-day key to aggregate.
|
|
995
|
+
* @returns unsorted per-session rows for the day.
|
|
996
|
+
*/
|
|
997
|
+
async scanSessionsProjections(dayKey) {
|
|
998
|
+
const { sessions, persistence, projections } = this.deps;
|
|
999
|
+
const projectionsService = projections?.();
|
|
1000
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1001
|
+
const liveIds = /* @__PURE__ */ new Set();
|
|
1002
|
+
if (sessions !== void 0) {
|
|
1003
|
+
const store = sessions();
|
|
1004
|
+
if (store !== void 0) for (const session of store.list()) {
|
|
1005
|
+
liveIds.add(session.id);
|
|
1006
|
+
const state = projectionsService?.stateOf(session, BILLING_UNIT_KEY);
|
|
1007
|
+
if (state !== void 0 && state.dayKey === dayKey) rows.set(session.id, {
|
|
1008
|
+
sessionId: session.id,
|
|
1009
|
+
title: foldSessionTitle(session.events),
|
|
1010
|
+
total: state.spend.total
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const persistenceService = persistence?.();
|
|
1015
|
+
if (persistenceService === void 0) return [...rows.values()];
|
|
1016
|
+
const snapshots = await persistenceService.listSnapshots();
|
|
1017
|
+
const pending = [];
|
|
1018
|
+
for (const { header, revision } of snapshots) {
|
|
1019
|
+
if (liveIds.has(header.id)) continue;
|
|
1020
|
+
const resolved = this.coldResolved.get(header.id);
|
|
1021
|
+
if (resolved !== void 0 && resolved.revision === revision) {
|
|
1022
|
+
if (resolved.value.dayKey === dayKey) rows.set(header.id, {
|
|
1023
|
+
sessionId: header.id,
|
|
1024
|
+
title: resolved.title,
|
|
1025
|
+
total: resolved.value.spend.total
|
|
1026
|
+
});
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
pending.push({
|
|
1030
|
+
id: header.id,
|
|
1031
|
+
revision
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
await withConcurrency(pending, 8, async ({ id, revision }) => {
|
|
1035
|
+
const resolved = await this.resolveCold(id);
|
|
1036
|
+
if (resolved !== void 0) this.coldResolved.set(id, {
|
|
1037
|
+
revision,
|
|
1038
|
+
...resolved
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
for (const { id } of pending) {
|
|
1042
|
+
const resolved = this.coldResolved.get(id);
|
|
1043
|
+
if (resolved !== void 0 && resolved.value.dayKey === dayKey) rows.set(id, {
|
|
1044
|
+
sessionId: id,
|
|
1045
|
+
title: resolved.title,
|
|
1046
|
+
total: resolved.value.spend.total
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
return [...rows.values()];
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Events-path per-session scan: price today's events in a single pass,
|
|
1053
|
+
* accumulating per session (per-event Beijing-day filter during collection,
|
|
1054
|
+
* hard cap), gated by revisions. Titles fold from each session's complete
|
|
1055
|
+
* log — a `session/title` event can predate today — so a rename is reflected
|
|
1056
|
+
* as soon as the session's log is re-read.
|
|
1057
|
+
* @param dayKey - the Beijing-time calendar-day key to aggregate.
|
|
1058
|
+
* @returns unsorted per-session rows for the day.
|
|
1059
|
+
*/
|
|
1060
|
+
async scanSessionsEvents(dayKey) {
|
|
1061
|
+
const { sessions, persistence, maxEvents, logger, billing, catalog } = this.deps;
|
|
1062
|
+
const names = new Map(catalog.map((model) => [model.id, model.name]));
|
|
1063
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1064
|
+
const liveIds = /* @__PURE__ */ new Set();
|
|
1065
|
+
let collected = 0;
|
|
1066
|
+
let truncated = false;
|
|
1067
|
+
const collect = (id, events) => {
|
|
1068
|
+
let row = rows.get(id);
|
|
1069
|
+
if (row === void 0) {
|
|
1070
|
+
row = {
|
|
1071
|
+
title: foldSessionTitle(events),
|
|
1072
|
+
total: 0
|
|
1073
|
+
};
|
|
1074
|
+
rows.set(id, row);
|
|
1075
|
+
}
|
|
1076
|
+
for (const event of events) {
|
|
1077
|
+
if (beijingDayKey(new Date(event.time)) !== dayKey) continue;
|
|
1078
|
+
collected += 1;
|
|
1079
|
+
if (collected > maxEvents) {
|
|
1080
|
+
truncated = true;
|
|
1081
|
+
return;
|
|
784
1082
|
}
|
|
1083
|
+
const priced = priceEvent(event, billing, names);
|
|
1084
|
+
if (priced !== void 0) row.total += priced.cost;
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
if (sessions !== void 0) {
|
|
1088
|
+
const store = sessions();
|
|
1089
|
+
if (store !== void 0) for (const session of store.list()) {
|
|
1090
|
+
liveIds.add(session.id);
|
|
1091
|
+
collect(session.id, session.events);
|
|
785
1092
|
if (truncated) break;
|
|
786
1093
|
}
|
|
787
1094
|
}
|
|
@@ -792,15 +1099,7 @@ var TodaySpendScanner = class {
|
|
|
792
1099
|
if (liveIds.has(header.id)) continue;
|
|
793
1100
|
if (this.lastEventsScan?.get(header.id) === revision) continue;
|
|
794
1101
|
try {
|
|
795
|
-
|
|
796
|
-
for (const event of inspection.events) {
|
|
797
|
-
if (beijingDayKey(new Date(event.time)) !== dayKey) continue;
|
|
798
|
-
events.push(event);
|
|
799
|
-
if (events.length >= maxEvents) {
|
|
800
|
-
truncated = true;
|
|
801
|
-
break;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
1102
|
+
collect(header.id, (await persistenceService.inspect(header.id)).events);
|
|
804
1103
|
} catch (error) {
|
|
805
1104
|
logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
|
|
806
1105
|
}
|
|
@@ -809,7 +1108,11 @@ var TodaySpendScanner = class {
|
|
|
809
1108
|
if (!truncated) this.lastEventsScan = new Map(snapshots.map((snapshot) => [snapshot.header.id, snapshot.revision]));
|
|
810
1109
|
}
|
|
811
1110
|
if (truncated) logger.warn(`llm-billing: today's events exceeded ${maxEvents}; result truncated`);
|
|
812
|
-
return
|
|
1111
|
+
return [...rows.entries()].filter(([, row]) => row.total > 0).map(([sessionId, row]) => ({
|
|
1112
|
+
sessionId,
|
|
1113
|
+
title: row.title,
|
|
1114
|
+
total: row.total
|
|
1115
|
+
}));
|
|
813
1116
|
}
|
|
814
1117
|
};
|
|
815
1118
|
//#endregion
|
|
@@ -829,6 +1132,11 @@ var TodaySpendScanner = class {
|
|
|
829
1132
|
* session-projection registry is composed, the plugin additionally registers
|
|
830
1133
|
* the `billingTodaySpend` projection unit, which folds each session's spend
|
|
831
1134
|
* eagerly and lets cold reads ride the projection-cache ladder.
|
|
1135
|
+
*
|
|
1136
|
+
* A per-session spend cache makes the badge's turn-settled recompute
|
|
1137
|
+
* incremental: session logs are append-only and chronological (the same
|
|
1138
|
+
* assumption the projection unit makes), so the spend is reused while the log
|
|
1139
|
+
* length is unchanged, and only the appended tail is priced when it grows.
|
|
832
1140
|
* @module @rayadesu/dsh-llm-billing
|
|
833
1141
|
*/
|
|
834
1142
|
const name = "llm-billing";
|
|
@@ -928,8 +1236,26 @@ function apply(ctx, config) {
|
|
|
928
1236
|
id: model.id,
|
|
929
1237
|
name: model.name ?? model.id
|
|
930
1238
|
}));
|
|
1239
|
+
const sessionSpendCache = /* @__PURE__ */ new Map();
|
|
931
1240
|
const fetchSessionSpend = async (sessionId) => {
|
|
932
|
-
|
|
1241
|
+
const events = await sessionEvents(ctx, sessionId);
|
|
1242
|
+
const cached = sessionSpendCache.get(sessionId);
|
|
1243
|
+
if (cached !== void 0 && cached.count === events.length) return cached.spend;
|
|
1244
|
+
if (cached !== void 0 && cached.count < events.length) {
|
|
1245
|
+
const spend = mergeTodaySpend(cached.spend, computeSessionSpend(events.slice(cached.count), billing, catalog));
|
|
1246
|
+
sessionSpendCache.set(sessionId, {
|
|
1247
|
+
count: events.length,
|
|
1248
|
+
spend
|
|
1249
|
+
});
|
|
1250
|
+
return spend;
|
|
1251
|
+
}
|
|
1252
|
+
const spend = computeSessionSpend(events, billing, catalog);
|
|
1253
|
+
if (sessionSpendCache.size >= 1024) sessionSpendCache.clear();
|
|
1254
|
+
sessionSpendCache.set(sessionId, {
|
|
1255
|
+
count: events.length,
|
|
1256
|
+
spend
|
|
1257
|
+
});
|
|
1258
|
+
return spend;
|
|
933
1259
|
};
|
|
934
1260
|
const unit = billingTodaySpendDefinition(billing, catalog);
|
|
935
1261
|
let unitRegistered = false;
|
|
@@ -953,12 +1279,19 @@ function apply(ctx, config) {
|
|
|
953
1279
|
catalog
|
|
954
1280
|
});
|
|
955
1281
|
const todayCache = new TodaySpendCache((dayKey) => scanner.scan(dayKey), TODAY_SPEND_CACHE_MS);
|
|
1282
|
+
const todaySessionsCache = new TodaySpendCache((dayKey) => scanner.scanSessions(dayKey), TODAY_SPEND_CACHE_MS);
|
|
956
1283
|
const fetchTodaySpend = async (force = false) => todayCache.get(force);
|
|
1284
|
+
const fetchTodaySessionsSpend = async (force = false) => todaySessionsCache.get(force);
|
|
1285
|
+
const fetchTurnSpend = async (sessionId, messageId) => {
|
|
1286
|
+
return computeTurnSpend(await sessionEvents(ctx, sessionId), billing, catalog, messageId);
|
|
1287
|
+
};
|
|
957
1288
|
new DeepSeekBalanceGateway(ctx, {
|
|
958
1289
|
fetchBalance,
|
|
959
1290
|
fetchSessionSpend,
|
|
960
|
-
fetchTodaySpend
|
|
1291
|
+
fetchTodaySpend,
|
|
1292
|
+
fetchTodaySessionsSpend,
|
|
1293
|
+
fetchTurnSpend
|
|
961
1294
|
});
|
|
962
1295
|
}
|
|
963
1296
|
//#endregion
|
|
964
|
-
export { BILLING_UNIT_KEY, Config, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, DeepSeekBalanceGateway, PUBLIC_BASE_URL, TODAY_SPEND_CACHE_MS, TODAY_SPEND_MAX_EVENTS, TodaySpendCache, TodaySpendScanner, addEventContribution, apply, beijingDayKey, billingTodaySpendDefinition, computeSessionSpend, computeTodaySpend, emptyTodaySpend, fetchDeepSeekBalance, foldBillingUnit, isPeak, mergeTodaySpend, name, parseDeepSeekBalance, priceEvent, resolveBilling };
|
|
1297
|
+
export { BILLING_UNIT_KEY, Config, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, DeepSeekBalanceGateway, PUBLIC_BASE_URL, SpendAccumulator, TODAY_SPEND_CACHE_MS, TODAY_SPEND_MAX_EVENTS, TodaySpendCache, TodaySpendScanner, addEventContribution, apply, beijingDayKey, billingTodaySpendDefinition, computeSessionSpend, computeTodaySpend, computeTurnSpend, emptyTodaySpend, fetchDeepSeekBalance, foldBillingUnit, foldSessionTitle, isPeak, mergeTodaySpend, name, parseDeepSeekBalance, priceEvent, resolveBilling };
|