@rayadesu/dsh-llm-billing 0.2.3 → 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 +295 -20
- 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 +19 -1
- package/lib/types/billing.js +44 -0
- package/lib/types/index.d.ts +2 -2
- package/lib/types/index.js +10 -4
- package/lib/types/today-spend.d.ts +54 -6
- package/lib/types/today-spend.js +214 -25
- package/lib/types/types.d.ts +23 -0
- package/package.json +1 -1
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# last confirmed-consistent state. Both languages carry equal authority; after
|
|
3
3
|
# editing either side, bring the other along and re-record both hashes with:
|
|
4
4
|
# git hash-object README.md README.zh.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 9fc18328397e8824803ad7ed5335ee4b8a1063fe
|
|
6
|
+
README.zh.md: 330b3ae531177a9b934137b4c2d1e32f949c16e0
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Add the plugin to a composition (a `cordis.yml` row) and give it a credential. I
|
|
|
16
16
|
# baseURL: https://api.deepseek.com
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The plugin registers the `billing` Remote with
|
|
19
|
+
The plugin registers the `billing` Remote with five methods: `getBalance()` (the parsed `/user/balance` snapshot), `getSessionSpend(sessionId)` (one session's billed cost), `getTodaySpend(force?)` (every session's billed cost on the current Beijing-time calendar day; `force` bypasses the host-side cache, for the badge's manual refresh), `getTodaySessionsSpend(force?)` (today's billed cost per session, sorted by cost descending, each row carrying the session's durable title), and `getTurnSpend(sessionId, messageId)` (one completed turn's billed cost, located by its closing assistant message id). The spend prices each `assistant/message` event's billed tokens (cache-hit input, cache-miss input including cache writes, and output including reasoning) at the official rate of the event's own Beijing-time peak/off-peak classification — peak windows apply weekdays (Monday–Friday) only, and weekends are always off-peak — then sums per model. A turn is the `turn/start`..`turn/end` range holding the closing message; the ranking folds each session's title from its latest `session/title` event (last-wins, so a rename is reflected as soon as its event commits and the session is re-read).
|
|
20
20
|
|
|
21
21
|
### Today-spend read path (no full scans per message)
|
|
22
22
|
|
package/README.zh.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
# baseURL: https://api.deepseek.com
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
插件注册 `billing` Remote
|
|
19
|
+
插件注册 `billing` Remote,含五个方法:`getBalance()`(解析后的 `/user/balance` 快照)、`getSessionSpend(sessionId)`(单个会话的计费花费)、`getTodaySpend(force?)`(当前北京时间自然日内所有会话的计费花费合计;`force` 绕过宿主侧缓存,供徽标手动刷新使用)、`getTodaySessionsSpend(force?)`(今日按会话的计费花费,按花费从高到低排序,每行带会话的持久标题)与 `getTurnSpend(sessionId, messageId)`(单个已完成回合的计费花费,按收尾助手消息 id 定位)。会话花费把每条 `assistant/message` 事件的计费 token(缓存命中输入、含缓存写入的未命中输入、含推理的输出)按事件自身发生时刻(北京时间)所在的峰/谷单价计价——高峰窗口仅周一至周五适用,周末全天按低谷价——再按模型汇总。一个回合即收尾消息所在的 `turn/start`..`turn/end` 区间;排行从每个会话日志里最后一条 `session/title` 事件折叠标题(last-wins,重命名事件一旦提交、会话被重新读取即反映新名字)。
|
|
20
20
|
|
|
21
21
|
### 今日花费读取路径(消息触发不再全量扫描)
|
|
22
22
|
|
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
|
|
@@ -527,6 +576,44 @@ function computeSessionSpend(events, billing, catalog) {
|
|
|
527
576
|
return priceEvents(events, billing, new Map(catalog.map((model) => [model.id, model.name])));
|
|
528
577
|
}
|
|
529
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 };
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
530
617
|
* Price every event whose Beijing-time calendar day is the day of `now`,
|
|
531
618
|
* aggregating across every session's event log. Events from other Beijing
|
|
532
619
|
* days are ignored, so a caller passes the concatenated logs of all sessions.
|
|
@@ -653,6 +740,23 @@ function foldBillingUnit(unit, events) {
|
|
|
653
740
|
* @module @rayadesu/dsh-llm-billing/today-spend
|
|
654
741
|
*/
|
|
655
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
|
+
/**
|
|
656
760
|
* Bounded parallel fan-out: run `run` over `items` with at most `limit` in
|
|
657
761
|
* flight. A shared index counter hands each worker its next job, so the
|
|
658
762
|
* dispatch is O(n) overall (array `shift()` would be O(n) per pop).
|
|
@@ -672,6 +776,8 @@ async function withConcurrency(items, limit, run) {
|
|
|
672
776
|
* coalesces concurrent misses, and a `force` bypass for the manual refresh
|
|
673
777
|
* path. Cross-day invalidation is automatic (the day key changes); a failed
|
|
674
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).
|
|
675
781
|
*/
|
|
676
782
|
var TodaySpendCache = class {
|
|
677
783
|
scan;
|
|
@@ -682,9 +788,9 @@ var TodaySpendCache = class {
|
|
|
682
788
|
cachedAt = 0;
|
|
683
789
|
inFlight;
|
|
684
790
|
/**
|
|
791
|
+
* @param scan - the aggregate computation behind a miss.
|
|
685
792
|
* @param ttlMs - time window in milliseconds (default 60 000).
|
|
686
793
|
* @param now - clock source (injectable for tests).
|
|
687
|
-
* @param scan - the aggregate computation behind a miss.
|
|
688
794
|
*/
|
|
689
795
|
constructor(scan, ttlMs = 6e4, now = () => /* @__PURE__ */ new Date()) {
|
|
690
796
|
this.scan = scan;
|
|
@@ -725,7 +831,7 @@ var TodaySpendCache = class {
|
|
|
725
831
|
*/
|
|
726
832
|
var TodaySpendScanner = class {
|
|
727
833
|
deps;
|
|
728
|
-
/** Cold sessions resolved on the projection path: id → revision + unit state. */
|
|
834
|
+
/** Cold sessions resolved on the projection path: id → revision + unit state + title. */
|
|
729
835
|
coldResolved = /* @__PURE__ */ new Map();
|
|
730
836
|
/** Cold sessions resolved on the events path: id → revision (events were collected). */
|
|
731
837
|
lastEventsScan;
|
|
@@ -742,11 +848,56 @@ var TodaySpendScanner = class {
|
|
|
742
848
|
this.deps.ensureUnit?.();
|
|
743
849
|
return this.scanProjections(dayKey);
|
|
744
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
|
+
}
|
|
745
897
|
/** Projection path: eager cells for live sessions, revision-gated cold ladder for the rest. */
|
|
746
898
|
async scanProjections(dayKey) {
|
|
747
|
-
const { sessions, persistence, projections
|
|
899
|
+
const { sessions, persistence, projections } = this.deps;
|
|
748
900
|
const projectionsService = projections?.();
|
|
749
|
-
const cache = projectionCache?.();
|
|
750
901
|
let total = emptyTodaySpend();
|
|
751
902
|
const liveIds = /* @__PURE__ */ new Set();
|
|
752
903
|
if (sessions !== void 0) {
|
|
@@ -774,21 +925,10 @@ var TodaySpendScanner = class {
|
|
|
774
925
|
});
|
|
775
926
|
}
|
|
776
927
|
await withConcurrency(pending, 8, async ({ id, revision }) => {
|
|
777
|
-
|
|
778
|
-
if (
|
|
779
|
-
value = (await cache.coldSnapshot(id)).values[BILLING_UNIT_KEY];
|
|
780
|
-
} catch (error) {
|
|
781
|
-
logger.warn(`llm-billing: projection cold read for session ${id} failed: ${String(error)}`);
|
|
782
|
-
}
|
|
783
|
-
if (value === void 0) try {
|
|
784
|
-
const inspection = await persistenceService.inspect(id);
|
|
785
|
-
value = foldBillingUnit(unit, inspection.events);
|
|
786
|
-
} catch (error) {
|
|
787
|
-
logger.warn(`llm-billing: skipping unreadable session ${id}: ${String(error)}`);
|
|
788
|
-
}
|
|
789
|
-
if (value !== void 0) this.coldResolved.set(id, {
|
|
928
|
+
const resolved = await this.resolveCold(id);
|
|
929
|
+
if (resolved !== void 0) this.coldResolved.set(id, {
|
|
790
930
|
revision,
|
|
791
|
-
|
|
931
|
+
...resolved
|
|
792
932
|
});
|
|
793
933
|
});
|
|
794
934
|
for (const { id } of pending) {
|
|
@@ -846,6 +986,134 @@ var TodaySpendScanner = class {
|
|
|
846
986
|
if (truncated) logger.warn(`llm-billing: today's events exceeded ${maxEvents}; result truncated`);
|
|
847
987
|
return accumulator.finish();
|
|
848
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;
|
|
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);
|
|
1092
|
+
if (truncated) break;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const persistenceService = persistence?.();
|
|
1096
|
+
if (!truncated && persistenceService !== void 0) {
|
|
1097
|
+
const snapshots = await persistenceService.listSnapshots();
|
|
1098
|
+
for (const { header, revision } of snapshots) {
|
|
1099
|
+
if (liveIds.has(header.id)) continue;
|
|
1100
|
+
if (this.lastEventsScan?.get(header.id) === revision) continue;
|
|
1101
|
+
try {
|
|
1102
|
+
collect(header.id, (await persistenceService.inspect(header.id)).events);
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
|
|
1105
|
+
}
|
|
1106
|
+
if (truncated) break;
|
|
1107
|
+
}
|
|
1108
|
+
if (!truncated) this.lastEventsScan = new Map(snapshots.map((snapshot) => [snapshot.header.id, snapshot.revision]));
|
|
1109
|
+
}
|
|
1110
|
+
if (truncated) logger.warn(`llm-billing: today's events exceeded ${maxEvents}; result truncated`);
|
|
1111
|
+
return [...rows.entries()].filter(([, row]) => row.total > 0).map(([sessionId, row]) => ({
|
|
1112
|
+
sessionId,
|
|
1113
|
+
title: row.title,
|
|
1114
|
+
total: row.total
|
|
1115
|
+
}));
|
|
1116
|
+
}
|
|
849
1117
|
};
|
|
850
1118
|
//#endregion
|
|
851
1119
|
//#region lib/types/index.js
|
|
@@ -1011,12 +1279,19 @@ function apply(ctx, config) {
|
|
|
1011
1279
|
catalog
|
|
1012
1280
|
});
|
|
1013
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);
|
|
1014
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
|
+
};
|
|
1015
1288
|
new DeepSeekBalanceGateway(ctx, {
|
|
1016
1289
|
fetchBalance,
|
|
1017
1290
|
fetchSessionSpend,
|
|
1018
|
-
fetchTodaySpend
|
|
1291
|
+
fetchTodaySpend,
|
|
1292
|
+
fetchTodaySessionsSpend,
|
|
1293
|
+
fetchTurnSpend
|
|
1019
1294
|
});
|
|
1020
1295
|
}
|
|
1021
1296
|
//#endregion
|
|
1022
|
-
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, 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 };
|
package/lib/typert.host.js
CHANGED
|
@@ -27,6 +27,14 @@ const _rayadesu_dsh_llm_billing_billing_getSessionSpend_result$schema = z.object
|
|
|
27
27
|
'outputCost': z.number(),
|
|
28
28
|
})),
|
|
29
29
|
})
|
|
30
|
+
const _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_parameter_0$schema = z.union([z.undefined(), z.literal(false), z.literal(true)])
|
|
31
|
+
const _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_result$schema = z.object({
|
|
32
|
+
'sessions': z.array(z.object({
|
|
33
|
+
'sessionId': z.intersection(z.string(), z.unknown()),
|
|
34
|
+
'title': z.union([z.literal(null), z.string()]),
|
|
35
|
+
'total': z.number(),
|
|
36
|
+
})),
|
|
37
|
+
})
|
|
30
38
|
const _rayadesu_dsh_llm_billing_billing_getTodaySpend_parameter_0$schema = z.union([z.undefined(), z.literal(false), z.literal(true)])
|
|
31
39
|
const _rayadesu_dsh_llm_billing_billing_getTodaySpend_result$schema = z.object({
|
|
32
40
|
'total': z.number(),
|
|
@@ -44,6 +52,11 @@ const _rayadesu_dsh_llm_billing_billing_getTodaySpend_result$schema = z.object({
|
|
|
44
52
|
'outputCost': z.number(),
|
|
45
53
|
})),
|
|
46
54
|
})
|
|
55
|
+
const _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_0$schema = z.intersection(z.string(), z.unknown())
|
|
56
|
+
const _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_1$schema = z.string()
|
|
57
|
+
const _rayadesu_dsh_llm_billing_billing_getTurnSpend_result$schema = z.object({
|
|
58
|
+
'total': z.number(),
|
|
59
|
+
})
|
|
47
60
|
|
|
48
61
|
export const TYPERT = {
|
|
49
62
|
package: '@rayadesu/dsh-llm-billing',
|
|
@@ -64,7 +77,7 @@ export const TYPERT = {
|
|
|
64
77
|
typeSymbol: '@rayadesu/dsh-llm-billing/types#DeepSeekBalance',
|
|
65
78
|
schema: _rayadesu_dsh_llm_billing_billing_getBalance_result$schema,
|
|
66
79
|
},
|
|
67
|
-
sourceLocation: {"file":"packages/llm-billing/src/balance.ts","line":
|
|
80
|
+
sourceLocation: {"file":"packages/llm-billing/src/balance.ts","line":154,"column":3},
|
|
68
81
|
},
|
|
69
82
|
{
|
|
70
83
|
id: '@rayadesu/dsh-llm-billing#billing/getSessionSpend',
|
|
@@ -89,7 +102,33 @@ export const TYPERT = {
|
|
|
89
102
|
typeSymbol: '@rayadesu/dsh-llm-billing/types#DeepSeekSessionSpend',
|
|
90
103
|
schema: _rayadesu_dsh_llm_billing_billing_getSessionSpend_result$schema,
|
|
91
104
|
},
|
|
92
|
-
sourceLocation: {"file":"packages/llm-billing/src/balance.ts","line":
|
|
105
|
+
sourceLocation: {"file":"packages/llm-billing/src/balance.ts","line":165,"column":3},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: '@rayadesu/dsh-llm-billing#billing/getTodaySessionsSpend',
|
|
109
|
+
service: 'billing',
|
|
110
|
+
namespace: 'billing',
|
|
111
|
+
method: 'getTodaySessionsSpend',
|
|
112
|
+
invocation: { kind: 'direct' },
|
|
113
|
+
parameters: [
|
|
114
|
+
{
|
|
115
|
+
name: 'force',
|
|
116
|
+
wire: 'force',
|
|
117
|
+
source: 'json',
|
|
118
|
+
acceptsUndefined: true,
|
|
119
|
+
codec: {
|
|
120
|
+
mode: 'strict',
|
|
121
|
+
typeSymbol: '@rayadesu/dsh-llm-billing#billing/getTodaySessionsSpend:force',
|
|
122
|
+
schema: _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_parameter_0$schema,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
result: {
|
|
127
|
+
mode: 'strict',
|
|
128
|
+
typeSymbol: '@rayadesu/dsh-llm-billing/types#DeepSeekTodaySessionsSpend',
|
|
129
|
+
schema: _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_result$schema,
|
|
130
|
+
},
|
|
131
|
+
sourceLocation: {"file":"packages/llm-billing/src/balance.ts","line":192,"column":3},
|
|
93
132
|
},
|
|
94
133
|
{
|
|
95
134
|
id: '@rayadesu/dsh-llm-billing#billing/getTodaySpend',
|
|
@@ -115,7 +154,42 @@ export const TYPERT = {
|
|
|
115
154
|
typeSymbol: '@rayadesu/dsh-llm-billing/types#DeepSeekTodaySpend',
|
|
116
155
|
schema: _rayadesu_dsh_llm_billing_billing_getTodaySpend_result$schema,
|
|
117
156
|
},
|
|
118
|
-
sourceLocation: {"file":"packages/llm-billing/src/balance.ts","line":
|
|
157
|
+
sourceLocation: {"file":"packages/llm-billing/src/balance.ts","line":178,"column":3},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: '@rayadesu/dsh-llm-billing#billing/getTurnSpend',
|
|
161
|
+
service: 'billing',
|
|
162
|
+
namespace: 'billing',
|
|
163
|
+
method: 'getTurnSpend',
|
|
164
|
+
invocation: { kind: 'direct' },
|
|
165
|
+
parameters: [
|
|
166
|
+
{
|
|
167
|
+
name: 'sessionId',
|
|
168
|
+
wire: 'sessionId',
|
|
169
|
+
source: 'json',
|
|
170
|
+
codec: {
|
|
171
|
+
mode: 'strict',
|
|
172
|
+
typeSymbol: '@rayadesu/dsh-llm-billing#billing/getTurnSpend:sessionId',
|
|
173
|
+
schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_0$schema,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'messageId',
|
|
178
|
+
wire: 'messageId',
|
|
179
|
+
source: 'json',
|
|
180
|
+
codec: {
|
|
181
|
+
mode: 'strict',
|
|
182
|
+
typeSymbol: '@rayadesu/dsh-llm-billing#billing/getTurnSpend:messageId',
|
|
183
|
+
schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_1$schema,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
result: {
|
|
188
|
+
mode: 'strict',
|
|
189
|
+
typeSymbol: '@rayadesu/dsh-llm-billing/types#DeepSeekTurnSpend',
|
|
190
|
+
schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_result$schema,
|
|
191
|
+
},
|
|
192
|
+
sourceLocation: {"file":"packages/llm-billing/src/balance.ts","line":205,"column":3},
|
|
119
193
|
},
|
|
120
194
|
],
|
|
121
195
|
model: {
|
|
@@ -3,18 +3,22 @@ import type {
|
|
|
3
3
|
RemoteResult,
|
|
4
4
|
TypertRemoteContribution,
|
|
5
5
|
} from '@deepseek-ai/dsh-typert-protocol'
|
|
6
|
-
import type { DeepSeekBalance, DeepSeekSessionSpend, DeepSeekTodaySpend } from '@rayadesu/dsh-llm-billing/types'
|
|
6
|
+
import type { DeepSeekBalance, DeepSeekSessionSpend, DeepSeekTodaySessionsSpend, DeepSeekTodaySpend, DeepSeekTurnSpend } from '@rayadesu/dsh-llm-billing/types'
|
|
7
7
|
|
|
8
8
|
declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
9
9
|
interface TypertRemoteNamespace$62696c6c696e67 {
|
|
10
10
|
getBalance: () => Promise<RemoteResult<DeepSeekBalance>>
|
|
11
11
|
getSessionSpend: (sessionId: SessionId) => Promise<RemoteResult<DeepSeekSessionSpend>>
|
|
12
|
+
getTodaySessionsSpend: (force?: boolean) => Promise<RemoteResult<DeepSeekTodaySessionsSpend>>
|
|
12
13
|
getTodaySpend: (force?: boolean) => Promise<RemoteResult<DeepSeekTodaySpend>>
|
|
14
|
+
getTurnSpend: (sessionId: SessionId, messageId: string) => Promise<RemoteResult<DeepSeekTurnSpend>>
|
|
13
15
|
}
|
|
14
16
|
interface TypertRemoteMap {
|
|
15
17
|
'billing/getBalance': () => Promise<RemoteResult<DeepSeekBalance>>
|
|
16
18
|
'billing/getSessionSpend': (sessionId: SessionId) => Promise<RemoteResult<DeepSeekSessionSpend>>
|
|
19
|
+
'billing/getTodaySessionsSpend': (force?: boolean) => Promise<RemoteResult<DeepSeekTodaySessionsSpend>>
|
|
17
20
|
'billing/getTodaySpend': (force?: boolean) => Promise<RemoteResult<DeepSeekTodaySpend>>
|
|
21
|
+
'billing/getTurnSpend': (sessionId: SessionId, messageId: string) => Promise<RemoteResult<DeepSeekTurnSpend>>
|
|
18
22
|
}
|
|
19
23
|
interface TypertRemoteNamespaceMap {
|
|
20
24
|
'billing': TypertRemoteNamespace$62696c6c696e67
|