@rayadesu/dsh-llm-billing 0.2.0 → 0.2.1
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/lib/index.js +3165 -587
- package/lib/invariant.js +3 -3
- package/lib/typert.host.js +18 -30
- package/lib/typert.remote-client.d.ts +3 -3
- package/lib/typert.remote-client.js +18 -30
- package/lib/types/balance.d.ts +6 -14
- package/lib/types/balance.js +5 -9
- package/lib/types/billing.d.ts +7 -88
- package/lib/types/billing.js +62 -171
- package/lib/types/index.d.ts +5 -22
- package/lib/types/index.js +53 -64
- package/lib/types/invariant.d.ts +2 -2
- package/lib/types/invariant.js +3 -3
- package/lib/types/types.d.ts +3 -3
- package/lib/types/types.js +1 -1
- package/package.json +1 -1
- package/lib/types/projection.d.ts +0 -69
- package/lib/types/projection.js +0 -88
- package/lib/types/today-spend.d.ts +0 -138
- package/lib/types/today-spend.js +0 -246
package/lib/types/billing.js
CHANGED
|
@@ -3,19 +3,9 @@
|
|
|
3
3
|
* pricing. Pure functions over session events and the pricing table, so the
|
|
4
4
|
* Remote gateway stays transport-free and the whole spend is testable without
|
|
5
5
|
* a key.
|
|
6
|
-
*
|
|
7
|
-
* The per-event pricing lives in {@link priceEvent}, the one shared fold
|
|
8
|
-
* primitive: the events-scan paths ({@link computeSessionSpend},
|
|
9
|
-
* {@link computeTodaySpend}) and the session-projection unit
|
|
10
|
-
* (`billingTodaySpend` in projection.ts) all fold the same contribution, so a
|
|
11
|
-
* pricing-table change cannot drift one path from the others.
|
|
12
|
-
* @module @rayadesu/dsh-llm-billing/billing
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
* Published peak-hour windows (Beijing time): 09:00–12:00 and 14:00–18:00,
|
|
16
|
-
* applied on weekdays (Monday–Friday) only — weekends are always off-peak
|
|
17
|
-
* (effective 2026-08-23).
|
|
6
|
+
* @module @deepseek-ai/dsh-llm-billing/billing
|
|
18
7
|
*/
|
|
8
|
+
/** Published peak-hour windows (Beijing time): 09:00–12:00 and 14:00–18:00. */
|
|
19
9
|
export const DEFAULT_PEAK_HOURS = [
|
|
20
10
|
{ start: 9, end: 12 },
|
|
21
11
|
{ start: 14, end: 18 },
|
|
@@ -32,13 +22,6 @@ export const DEFAULT_MODEL_PRICING = [
|
|
|
32
22
|
peak: { cacheHitInput: 0.30, cacheMissInput: 9.0, output: 27.0 },
|
|
33
23
|
offPeak: { cacheHitInput: 0.15, cacheMissInput: 4.5, output: 13.5 },
|
|
34
24
|
},
|
|
35
|
-
// deepseek-v4-flash-vision-exp bills at the same rates as deepseek-v4-flash;
|
|
36
|
-
// images are converted to tokens at the same per-token price.
|
|
37
|
-
{
|
|
38
|
-
model: 'deepseek-v4-flash-vision-exp',
|
|
39
|
-
peak: { cacheHitInput: 0.10, cacheMissInput: 3.0, output: 9.0 },
|
|
40
|
-
offPeak: { cacheHitInput: 0.05, cacheMissInput: 1.5, output: 4.5 },
|
|
41
|
-
},
|
|
42
25
|
];
|
|
43
26
|
/**
|
|
44
27
|
* Resolve optional configuration to a pricing table, defaulting omitted or
|
|
@@ -65,157 +48,23 @@ export function resolveBilling(config) {
|
|
|
65
48
|
function beijingHour(now) {
|
|
66
49
|
return new Date(now.getTime() + 8 * 3_600_000).getUTCHours();
|
|
67
50
|
}
|
|
68
|
-
/**
|
|
69
|
-
* The Beijing (Asia/Shanghai, UTC+8, no DST) weekday of a timestamp, as
|
|
70
|
-
* `getUTCDay()`: `0` is Sunday, `6` is Saturday.
|
|
71
|
-
*/
|
|
72
|
-
function beijingWeekday(now) {
|
|
73
|
-
return new Date(now.getTime() + 8 * 3_600_000).getUTCDay();
|
|
74
|
-
}
|
|
75
51
|
/** The Beijing (Asia/Shanghai, UTC+8, no DST) calendar-day key of a timestamp. */
|
|
76
|
-
|
|
52
|
+
function beijingDayKey(now) {
|
|
77
53
|
return new Date(now.getTime() + 8 * 3_600_000).toISOString().slice(0, 10);
|
|
78
54
|
}
|
|
79
55
|
/**
|
|
80
|
-
* Whether a timestamp falls inside any peak-hour window (Beijing time
|
|
81
|
-
* weekdays Monday–Friday only). Weekends (Saturday and Sunday) are always
|
|
82
|
-
* off-peak, matching the published peak-hours rule.
|
|
56
|
+
* Whether a timestamp falls inside any peak-hour window (Beijing time).
|
|
83
57
|
* @param billing - resolved pricing with peak-hour windows.
|
|
84
58
|
* @param now - the moment to classify.
|
|
85
|
-
* @returns true during
|
|
59
|
+
* @returns true during peak hours.
|
|
86
60
|
*/
|
|
87
61
|
export function isPeak(billing, now) {
|
|
88
|
-
const weekday = beijingWeekday(now);
|
|
89
|
-
if (weekday === 0 || weekday === 6)
|
|
90
|
-
return false;
|
|
91
62
|
const hour = beijingHour(now);
|
|
92
63
|
return billing.peakHours.some(({ start, end }) => hour >= start && hour < end);
|
|
93
64
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Price one event at the official per-model rates, applying the peak/off-peak
|
|
96
|
-
* table by its Beijing-time hour and weekday (peak windows apply Monday–Friday
|
|
97
|
-
* only; weekends are off-peak). Each `assistant/message` event with usage
|
|
98
|
-
* contributes cache-hit input, cache-miss input (uncached input plus cache
|
|
99
|
-
* writes), and output (reasoning included) tokens at the rate of its own
|
|
100
|
-
* timestamp; a model with usage but no pricing row contributes nothing (the
|
|
101
|
-
* published table prices only the two V4 rows).
|
|
102
|
-
* @param event - the event to price.
|
|
103
|
-
* @param billing - resolved pricing with peak-hour windows.
|
|
104
|
-
* @param names - model id → display label.
|
|
105
|
-
* @returns the priced contribution, or `undefined` when the event has no priced usage.
|
|
106
|
-
*/
|
|
107
|
-
export function priceEvent(event, billing, names) {
|
|
108
|
-
if (event.type !== 'assistant/message')
|
|
109
|
-
return undefined;
|
|
110
|
-
const reported = event.data.usage;
|
|
111
|
-
if (reported === undefined)
|
|
112
|
-
return undefined;
|
|
113
|
-
const model = event.data.message.source.model;
|
|
114
|
-
const pricing = billing.models.get(model);
|
|
115
|
-
if (pricing === undefined)
|
|
116
|
-
return undefined;
|
|
117
|
-
const time = new Date(event.time);
|
|
118
|
-
const peak = isPeak(billing, time);
|
|
119
|
-
const price = peak ? pricing.peak : pricing.offPeak;
|
|
120
|
-
const hit = reported.cacheReadTokens ?? 0;
|
|
121
|
-
const miss = reported.inputTokens + (reported.cacheWriteTokens ?? 0);
|
|
122
|
-
const output = reported.outputTokens;
|
|
123
|
-
const hitCost = (hit * price.cacheHitInput) / 1_000_000;
|
|
124
|
-
const missCost = (miss * price.cacheMissInput) / 1_000_000;
|
|
125
|
-
const outputCost = (output * price.output) / 1_000_000;
|
|
126
|
-
const cost = hitCost + missCost + outputCost;
|
|
127
|
-
return {
|
|
128
|
-
dayKey: beijingDayKey(time),
|
|
129
|
-
model,
|
|
130
|
-
displayName: names.get(model) ?? model,
|
|
131
|
-
cost,
|
|
132
|
-
peakCost: peak ? cost : 0,
|
|
133
|
-
offPeakCost: peak ? 0 : cost,
|
|
134
|
-
cacheHitInputTokens: hit,
|
|
135
|
-
cacheMissInputTokens: miss,
|
|
136
|
-
outputTokens: output,
|
|
137
|
-
cacheHitInputCost: hitCost,
|
|
138
|
-
cacheMissInputCost: missCost,
|
|
139
|
-
outputCost,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
/** A spend with no priced usage. */
|
|
143
|
-
export function emptyTodaySpend() {
|
|
144
|
-
return { total: 0, models: [] };
|
|
145
|
-
}
|
|
146
|
-
/** The today-spend shape of a single priced contribution. */
|
|
147
|
-
function contributionModel(priced) {
|
|
148
|
-
return {
|
|
149
|
-
model: priced.model,
|
|
150
|
-
displayName: priced.displayName,
|
|
151
|
-
cost: priced.cost,
|
|
152
|
-
peakCost: priced.peakCost,
|
|
153
|
-
offPeakCost: priced.offPeakCost,
|
|
154
|
-
cacheHitInputTokens: priced.cacheHitInputTokens,
|
|
155
|
-
cacheMissInputTokens: priced.cacheMissInputTokens,
|
|
156
|
-
outputTokens: priced.outputTokens,
|
|
157
|
-
cacheHitInputCost: priced.cacheHitInputCost,
|
|
158
|
-
cacheMissInputCost: priced.cacheMissInputCost,
|
|
159
|
-
outputCost: priced.outputCost,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Merge one priced event's contribution into an accumulator spend (pure:
|
|
164
|
-
* returns a new spend, never mutates its input).
|
|
165
|
-
* @param spend - the accumulator (per session and day, or across sessions).
|
|
166
|
-
* @param priced - the priced contribution to add.
|
|
167
|
-
* @returns the merged spend.
|
|
168
|
-
*/
|
|
169
|
-
export function addEventContribution(spend, priced) {
|
|
170
|
-
const rows = spend.models.map(row => row.model === priced.model
|
|
171
|
-
? {
|
|
172
|
-
...row,
|
|
173
|
-
cost: row.cost + priced.cost,
|
|
174
|
-
peakCost: row.peakCost + priced.peakCost,
|
|
175
|
-
offPeakCost: row.offPeakCost + priced.offPeakCost,
|
|
176
|
-
cacheHitInputTokens: row.cacheHitInputTokens + priced.cacheHitInputTokens,
|
|
177
|
-
cacheMissInputTokens: row.cacheMissInputTokens + priced.cacheMissInputTokens,
|
|
178
|
-
outputTokens: row.outputTokens + priced.outputTokens,
|
|
179
|
-
cacheHitInputCost: row.cacheHitInputCost + priced.cacheHitInputCost,
|
|
180
|
-
cacheMissInputCost: row.cacheMissInputCost + priced.cacheMissInputCost,
|
|
181
|
-
outputCost: row.outputCost + priced.outputCost,
|
|
182
|
-
}
|
|
183
|
-
: row);
|
|
184
|
-
if (!rows.some(row => row.model === priced.model))
|
|
185
|
-
rows.push(contributionModel(priced));
|
|
186
|
-
return { total: spend.total + priced.cost, models: rows };
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Sum two spends (per session and day, or across sessions) into one (pure:
|
|
190
|
-
* returns a new spend, never mutates its inputs).
|
|
191
|
-
* @param target - the accumulator spend.
|
|
192
|
-
* @param source - the spend to add.
|
|
193
|
-
* @returns the summed spend.
|
|
194
|
-
*/
|
|
195
|
-
export function mergeTodaySpend(target, source) {
|
|
196
|
-
let merged = target;
|
|
197
|
-
for (const row of source.models) {
|
|
198
|
-
merged = addEventContribution(merged, {
|
|
199
|
-
dayKey: '',
|
|
200
|
-
model: row.model,
|
|
201
|
-
displayName: row.displayName,
|
|
202
|
-
cost: row.cost,
|
|
203
|
-
peakCost: row.peakCost,
|
|
204
|
-
offPeakCost: row.offPeakCost,
|
|
205
|
-
cacheHitInputTokens: row.cacheHitInputTokens,
|
|
206
|
-
cacheMissInputTokens: row.cacheMissInputTokens,
|
|
207
|
-
outputTokens: row.outputTokens,
|
|
208
|
-
cacheHitInputCost: row.cacheHitInputCost,
|
|
209
|
-
cacheMissInputCost: row.cacheMissInputCost,
|
|
210
|
-
outputCost: row.outputCost,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
return merged;
|
|
214
|
-
}
|
|
215
65
|
/**
|
|
216
66
|
* Price a set of billed events at the official per-model rates, applying the
|
|
217
|
-
* peak/off-peak table per event by its Beijing-time hour
|
|
218
|
-
* windows apply Monday–Friday only; weekends are off-peak). Each
|
|
67
|
+
* peak/off-peak table per event by its Beijing-time hour. Each
|
|
219
68
|
* `assistant/message` event with usage contributes cache-hit input, cache-miss
|
|
220
69
|
* input (uncached input plus cache writes), and output (reasoning included)
|
|
221
70
|
* tokens at the rate of its own timestamp, with the three component costs
|
|
@@ -228,14 +77,64 @@ export function mergeTodaySpend(target, source) {
|
|
|
228
77
|
*/
|
|
229
78
|
function priceEvents(events, billing, catalog) {
|
|
230
79
|
const names = new Map(catalog.map(model => [model.id, model.name]));
|
|
231
|
-
|
|
80
|
+
const rows = new Map();
|
|
232
81
|
for (const event of events) {
|
|
233
|
-
|
|
234
|
-
|
|
82
|
+
if (event.type !== 'assistant/message')
|
|
83
|
+
continue;
|
|
84
|
+
const reported = event.data.usage;
|
|
85
|
+
if (reported === undefined)
|
|
235
86
|
continue;
|
|
236
|
-
|
|
87
|
+
const model = event.data.message.source.model;
|
|
88
|
+
const pricing = billing.models.get(model);
|
|
89
|
+
if (pricing === undefined)
|
|
90
|
+
continue;
|
|
91
|
+
const peak = isPeak(billing, new Date(event.time));
|
|
92
|
+
const price = peak ? pricing.peak : pricing.offPeak;
|
|
93
|
+
const hit = reported.cacheReadTokens ?? 0;
|
|
94
|
+
const miss = reported.inputTokens + (reported.cacheWriteTokens ?? 0);
|
|
95
|
+
const output = reported.outputTokens;
|
|
96
|
+
const hitCost = (hit * price.cacheHitInput) / 1_000_000;
|
|
97
|
+
const missCost = (miss * price.cacheMissInput) / 1_000_000;
|
|
98
|
+
const outputCost = (output * price.output) / 1_000_000;
|
|
99
|
+
const cost = hitCost + missCost + outputCost;
|
|
100
|
+
let row = rows.get(model);
|
|
101
|
+
if (row === undefined) {
|
|
102
|
+
row = {
|
|
103
|
+
cacheHitInputTokens: 0, cacheMissInputTokens: 0, outputTokens: 0,
|
|
104
|
+
cost: 0, peakCost: 0, offPeakCost: 0,
|
|
105
|
+
cacheHitInputCost: 0, cacheMissInputCost: 0, outputCost: 0,
|
|
106
|
+
};
|
|
107
|
+
rows.set(model, row);
|
|
108
|
+
}
|
|
109
|
+
row.cacheHitInputTokens += hit;
|
|
110
|
+
row.cacheMissInputTokens += miss;
|
|
111
|
+
row.outputTokens += output;
|
|
112
|
+
row.cost += cost;
|
|
113
|
+
row.cacheHitInputCost += hitCost;
|
|
114
|
+
row.cacheMissInputCost += missCost;
|
|
115
|
+
row.outputCost += outputCost;
|
|
116
|
+
if (peak)
|
|
117
|
+
row.peakCost += cost;
|
|
118
|
+
else
|
|
119
|
+
row.offPeakCost += cost;
|
|
237
120
|
}
|
|
238
|
-
|
|
121
|
+
const models = [...rows.entries()].map(([model, row]) => ({
|
|
122
|
+
model,
|
|
123
|
+
displayName: names.get(model) ?? model,
|
|
124
|
+
cost: row.cost,
|
|
125
|
+
peakCost: row.peakCost,
|
|
126
|
+
offPeakCost: row.offPeakCost,
|
|
127
|
+
cacheHitInputTokens: row.cacheHitInputTokens,
|
|
128
|
+
cacheMissInputTokens: row.cacheMissInputTokens,
|
|
129
|
+
outputTokens: row.outputTokens,
|
|
130
|
+
cacheHitInputCost: row.cacheHitInputCost,
|
|
131
|
+
cacheMissInputCost: row.cacheMissInputCost,
|
|
132
|
+
outputCost: row.outputCost,
|
|
133
|
+
}));
|
|
134
|
+
return {
|
|
135
|
+
total: models.reduce((sum, model) => sum + model.cost, 0),
|
|
136
|
+
models,
|
|
137
|
+
};
|
|
239
138
|
}
|
|
240
139
|
/**
|
|
241
140
|
* Price one session's complete event log at the official per-model rates.
|
|
@@ -259,14 +158,6 @@ export function computeSessionSpend(events, billing, catalog) {
|
|
|
259
158
|
*/
|
|
260
159
|
export function computeTodaySpend(events, billing, catalog, now = new Date()) {
|
|
261
160
|
const day = beijingDayKey(now);
|
|
262
|
-
|
|
263
|
-
let spend = emptyTodaySpend();
|
|
264
|
-
for (const event of events) {
|
|
265
|
-
const priced = priceEvent(event, billing, names);
|
|
266
|
-
if (priced === undefined || priced.dayKey !== day)
|
|
267
|
-
continue;
|
|
268
|
-
spend = addEventContribution(spend, priced);
|
|
269
|
-
}
|
|
270
|
-
return spend;
|
|
161
|
+
return priceEvents(events.filter(event => beijingDayKey(new Date(event.time)) === day), billing, catalog);
|
|
271
162
|
}
|
|
272
163
|
//# sourceMappingURL=billing.js.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,28 +4,15 @@
|
|
|
4
4
|
* the credential/environment seams, prices each session's billed usage with the
|
|
5
5
|
* peak/off-peak table, and exposes the `billing` Remote (`getBalance`, the
|
|
6
6
|
* per-session `getSessionSpend`, and the all-sessions `getTodaySpend`).
|
|
7
|
-
*
|
|
8
|
-
* Today's spend never scans every session log per request: a 60-second
|
|
9
|
-
* Beijing-day cache with in-flight coalescing serves the message-triggered
|
|
10
|
-
* reads, the manual refresh may bypass the time window (`force`), and the
|
|
11
|
-
* computation behind a miss reads only sessions whose persisted revision
|
|
12
|
-
* changed since the last resolution (see today-spend.ts). When the
|
|
13
|
-
* session-projection registry is composed, the plugin additionally registers
|
|
14
|
-
* the `billingTodaySpend` projection unit, which folds each session's spend
|
|
15
|
-
* eagerly and lets cold reads ride the projection-cache ladder.
|
|
16
|
-
* @module @rayadesu/dsh-llm-billing
|
|
7
|
+
* @module @deepseek-ai/dsh-llm-billing
|
|
17
8
|
*/
|
|
18
9
|
import type { Context } from '@deepseek-ai/cordis';
|
|
19
10
|
import z from '@deepseek-ai/schemastery';
|
|
20
11
|
import type { BillingConfig } from './billing.ts';
|
|
21
12
|
export { DeepSeekBalanceGateway, fetchDeepSeekBalance, parseDeepSeekBalance } from './balance.ts';
|
|
22
|
-
export {
|
|
23
|
-
export type { BillingConfig, BillingConfigModel,
|
|
13
|
+
export { computeSessionSpend, computeTodaySpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, isPeak, resolveBilling, } from './billing.ts';
|
|
14
|
+
export type { BillingConfig, BillingConfigModel, DeepSeekModelPricing, DeepSeekTokenPrice, PeakHourWindow, ResolvedBilling, } from './billing.ts';
|
|
24
15
|
export type * from './types.ts';
|
|
25
|
-
export { BILLING_UNIT_KEY, billingTodaySpendDefinition, foldBillingUnit } from './projection.ts';
|
|
26
|
-
export type { BillingUnitState } from './projection.ts';
|
|
27
|
-
export { TodaySpendCache, TodaySpendScanner } from './today-spend.ts';
|
|
28
|
-
export type { ScannerPersistedHeader, ScannerSession, TodaySpendScannerDeps } from './today-spend.ts';
|
|
29
16
|
export declare const name = "llm-billing";
|
|
30
17
|
/** Public API default; deployments may point elsewhere via $DEEPSEEK_BASE_URL. */
|
|
31
18
|
export declare const PUBLIC_BASE_URL = "https://api.deepseek.com";
|
|
@@ -47,16 +34,12 @@ export interface Config {
|
|
|
47
34
|
apiKeyEnv?: string;
|
|
48
35
|
/** Endpoint base; defaults to `$DEEPSEEK_BASE_URL`, then `https://api.deepseek.com`. */
|
|
49
36
|
baseURL?: string;
|
|
50
|
-
/** Advisory display rows, in presentation order; defaults to V4 Flash
|
|
37
|
+
/** Advisory display rows, in presentation order; defaults to V4 Flash and V4 Pro. */
|
|
51
38
|
models?: BillingModel[];
|
|
52
|
-
/** Pricing table and peak-hour windows; omission uses the published defaults.
|
|
39
|
+
/** Pricing table and peak-hour windows; omission uses the published defaults. */
|
|
53
40
|
billing?: BillingConfig;
|
|
54
41
|
}
|
|
55
42
|
export declare const Config: z<Config>;
|
|
56
|
-
/** How often a Beijing-day "today spend" value may be recomputed (60s). */
|
|
57
|
-
export declare const TODAY_SPEND_CACHE_MS = 60000;
|
|
58
|
-
/** Hard cap on today's events collected by the events scan path. */
|
|
59
|
-
export declare const TODAY_SPEND_MAX_EVENTS = 200000;
|
|
60
43
|
/**
|
|
61
44
|
* Register the `billing` Remote under the `billing` namespace.
|
|
62
45
|
* @param ctx - owning plugin context.
|
package/lib/types/index.js
CHANGED
|
@@ -4,29 +4,16 @@
|
|
|
4
4
|
* the credential/environment seams, prices each session's billed usage with the
|
|
5
5
|
* peak/off-peak table, and exposes the `billing` Remote (`getBalance`, the
|
|
6
6
|
* per-session `getSessionSpend`, and the all-sessions `getTodaySpend`).
|
|
7
|
-
*
|
|
8
|
-
* Today's spend never scans every session log per request: a 60-second
|
|
9
|
-
* Beijing-day cache with in-flight coalescing serves the message-triggered
|
|
10
|
-
* reads, the manual refresh may bypass the time window (`force`), and the
|
|
11
|
-
* computation behind a miss reads only sessions whose persisted revision
|
|
12
|
-
* changed since the last resolution (see today-spend.ts). When the
|
|
13
|
-
* session-projection registry is composed, the plugin additionally registers
|
|
14
|
-
* the `billingTodaySpend` projection unit, which folds each session's spend
|
|
15
|
-
* eagerly and lets cold reads ride the projection-cache ladder.
|
|
16
|
-
* @module @rayadesu/dsh-llm-billing
|
|
7
|
+
* @module @deepseek-ai/dsh-llm-billing
|
|
17
8
|
*/
|
|
18
9
|
import z from '@deepseek-ai/schemastery';
|
|
19
10
|
import { assertUsableApiKey, LlmError } from '@deepseek-ai/dsh-llm';
|
|
20
11
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
21
12
|
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
|
|
22
13
|
import { DeepSeekBalanceGateway, fetchDeepSeekBalance } from "./balance.js";
|
|
23
|
-
import { computeSessionSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, resolveBilling, } from "./billing.js";
|
|
24
|
-
import { billingTodaySpendDefinition } from "./projection.js";
|
|
25
|
-
import { TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
|
|
14
|
+
import { computeSessionSpend, computeTodaySpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, resolveBilling, } from "./billing.js";
|
|
26
15
|
export { DeepSeekBalanceGateway, fetchDeepSeekBalance, parseDeepSeekBalance } from "./balance.js";
|
|
27
|
-
export {
|
|
28
|
-
export { BILLING_UNIT_KEY, billingTodaySpendDefinition, foldBillingUnit } from "./projection.js";
|
|
29
|
-
export { TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
|
|
16
|
+
export { computeSessionSpend, computeTodaySpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, isPeak, resolveBilling, } from "./billing.js";
|
|
30
17
|
export const name = 'llm-billing';
|
|
31
18
|
const DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY';
|
|
32
19
|
const BASE_URL_ENV = 'DEEPSEEK_BASE_URL';
|
|
@@ -35,7 +22,6 @@ export const PUBLIC_BASE_URL = 'https://api.deepseek.com';
|
|
|
35
22
|
const DEFAULT_MODELS = [
|
|
36
23
|
{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash' },
|
|
37
24
|
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' },
|
|
38
|
-
{ id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp' },
|
|
39
25
|
];
|
|
40
26
|
const billingModel = z.object({
|
|
41
27
|
id: z.string().required(),
|
|
@@ -63,14 +49,9 @@ export const Config = z.object({
|
|
|
63
49
|
models: z.array(billingModel).default(DEFAULT_MODELS),
|
|
64
50
|
billing: billingConfig,
|
|
65
51
|
});
|
|
66
|
-
/** How often a Beijing-day "today spend" value may be recomputed (60s). */
|
|
67
|
-
export const TODAY_SPEND_CACHE_MS = 60_000;
|
|
68
|
-
/** Hard cap on today's events collected by the events scan path. */
|
|
69
|
-
export const TODAY_SPEND_MAX_EVENTS = 200_000;
|
|
70
52
|
/**
|
|
71
53
|
* Read one session's event log: the live SessionStore first, then the
|
|
72
|
-
* persistence backend for a flushed session
|
|
73
|
-
* header listing).
|
|
54
|
+
* persistence backend for a flushed session.
|
|
74
55
|
* @param ctx - plugin context carrying the SessionStore and optional persistence.
|
|
75
56
|
* @param sessionId - the session to read.
|
|
76
57
|
* @returns the session's complete event log.
|
|
@@ -83,15 +64,53 @@ async function sessionEvents(ctx, sessionId) {
|
|
|
83
64
|
return live.events;
|
|
84
65
|
const persistence = ctx.get('sessionPersistence');
|
|
85
66
|
if (persistence !== undefined) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
67
|
+
for (const header of await persistence.list()) {
|
|
68
|
+
if (header.id !== sessionId)
|
|
69
|
+
continue;
|
|
70
|
+
const inspection = await persistence.inspect(sessionId);
|
|
71
|
+
return inspection.events;
|
|
91
72
|
}
|
|
92
73
|
}
|
|
93
74
|
throw new LlmError(`llm-billing: session ${sessionId} not found`, 'NOT_FOUND');
|
|
94
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Read every session's event log, concatenated: each live SessionStore
|
|
78
|
+
* session first (its log may hold events not yet flushed), then each persisted
|
|
79
|
+
* session that is not live, so no event is counted twice. Events are appended
|
|
80
|
+
* one at a time: spreading a very large log into `push(...)` exceeds the
|
|
81
|
+
* engine's argument limit and throws a stack RangeError.
|
|
82
|
+
* @param ctx - plugin context carrying the SessionStore and optional persistence.
|
|
83
|
+
* @returns every session's complete event log, concatenated.
|
|
84
|
+
*/
|
|
85
|
+
async function allSessionEvents(ctx) {
|
|
86
|
+
const events = [];
|
|
87
|
+
const sessions = ctx.get('sessions');
|
|
88
|
+
const liveIds = new Set();
|
|
89
|
+
if (sessions !== undefined) {
|
|
90
|
+
for (const session of sessions.list()) {
|
|
91
|
+
liveIds.add(session.id);
|
|
92
|
+
for (const event of session.events)
|
|
93
|
+
events.push(event);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const persistence = ctx.get('sessionPersistence');
|
|
97
|
+
if (persistence !== undefined) {
|
|
98
|
+
for (const header of await persistence.list()) {
|
|
99
|
+
if (liveIds.has(header.id))
|
|
100
|
+
continue;
|
|
101
|
+
try {
|
|
102
|
+
const inspection = await persistence.inspect(header.id);
|
|
103
|
+
for (const event of inspection.events)
|
|
104
|
+
events.push(event);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// One unreadable session must not blank the whole-day aggregate.
|
|
108
|
+
ctx.logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return events;
|
|
113
|
+
}
|
|
95
114
|
/**
|
|
96
115
|
* Register the `billing` Remote under the `billing` namespace.
|
|
97
116
|
* @param ctx - owning plugin context.
|
|
@@ -121,46 +140,16 @@ export function apply(ctx, config) {
|
|
|
121
140
|
const apiKey = await resolveApiKey();
|
|
122
141
|
return fetchDeepSeekBalance(baseURL(), apiKey);
|
|
123
142
|
};
|
|
124
|
-
const billing = resolveBilling(config.billing);
|
|
125
|
-
const catalog = (config.models ?? DEFAULT_MODELS).map(model => ({ id: model.id, name: model.name ?? model.id }));
|
|
126
143
|
const fetchSessionSpend = async (sessionId) => {
|
|
144
|
+
const billing = resolveBilling(config.billing);
|
|
145
|
+
const catalog = (config.models ?? DEFAULT_MODELS).map(model => ({ id: model.id, name: model.name ?? model.id }));
|
|
127
146
|
return computeSessionSpend(await sessionEvents(ctx, sessionId), billing, catalog);
|
|
128
147
|
};
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// registry builds cells lazily over the in-memory log, so events committed
|
|
134
|
-
// before registration are folded on first touch; without the registry the
|
|
135
|
-
// events path serves today's spend.
|
|
136
|
-
const unit = billingTodaySpendDefinition(billing, catalog);
|
|
137
|
-
let unitRegistered = false;
|
|
138
|
-
const ensureUnit = () => {
|
|
139
|
-
if (unitRegistered)
|
|
140
|
-
return;
|
|
141
|
-
const registry = ctx.get('sessionProjections');
|
|
142
|
-
if (registry === undefined)
|
|
143
|
-
return;
|
|
144
|
-
registry.register(unit);
|
|
145
|
-
unitRegistered = true;
|
|
148
|
+
const fetchTodaySpend = async () => {
|
|
149
|
+
const billing = resolveBilling(config.billing);
|
|
150
|
+
const catalog = (config.models ?? DEFAULT_MODELS).map(model => ({ id: model.id, name: model.name ?? model.id }));
|
|
151
|
+
return computeTodaySpend(await allSessionEvents(ctx), billing, catalog);
|
|
146
152
|
};
|
|
147
|
-
// Plans A1–A3: 60s Beijing-day cache with in-flight coalescing and a force
|
|
148
|
-
// bypass, over a revision-gated scanner (projection path when the registry
|
|
149
|
-
// is composed, events path otherwise).
|
|
150
|
-
const scanner = new TodaySpendScanner({
|
|
151
|
-
sessions: () => ctx.get('sessions'),
|
|
152
|
-
persistence: () => ctx.get('sessionPersistence'),
|
|
153
|
-
projections: () => ctx.get('sessionProjections'),
|
|
154
|
-
projectionCache: () => ctx.get('sessionProjectionCache'),
|
|
155
|
-
ensureUnit,
|
|
156
|
-
unit,
|
|
157
|
-
maxEvents: TODAY_SPEND_MAX_EVENTS,
|
|
158
|
-
logger: ctx.logger,
|
|
159
|
-
billing,
|
|
160
|
-
catalog,
|
|
161
|
-
});
|
|
162
|
-
const todayCache = new TodaySpendCache(dayKey => scanner.scan(dayKey), TODAY_SPEND_CACHE_MS);
|
|
163
|
-
const fetchTodaySpend = async (force = false) => todayCache.get(force);
|
|
164
153
|
new DeepSeekBalanceGateway(ctx, { fetchBalance, fetchSessionSpend, fetchTodaySpend });
|
|
165
154
|
}
|
|
166
155
|
//# sourceMappingURL=index.js.map
|
package/lib/types/invariant.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Package-owned invariant companion for `@
|
|
3
|
-
* @module @
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-llm-billing`.
|
|
3
|
+
* @module @deepseek-ai/dsh-llm-billing/invariant
|
|
4
4
|
*/
|
|
5
5
|
import type { Context } from '@deepseek-ai/cordis';
|
|
6
6
|
/** Cordis companion plugin name. */
|
package/lib/types/invariant.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Package-owned invariant companion for `@
|
|
3
|
-
* @module @
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-llm-billing`.
|
|
3
|
+
* @module @deepseek-ai/dsh-llm-billing/invariant
|
|
4
4
|
*/
|
|
5
|
-
const PACKAGE_NAME = '@
|
|
5
|
+
const PACKAGE_NAME = '@deepseek-ai/dsh-llm-billing';
|
|
6
6
|
/** Cordis companion plugin name. */
|
|
7
7
|
export const name = 'llm-billing-invariant';
|
|
8
8
|
/** Service required before the companion can reserve package ownership. */
|
package/lib/types/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-safe balance and spend vocabulary shared by the `billing` Remote,
|
|
3
3
|
* its generated artifacts, and the web UI.
|
|
4
|
-
* @module @
|
|
4
|
+
* @module @deepseek-ai/dsh-llm-billing/types
|
|
5
5
|
*/
|
|
6
6
|
/** One currency line of the account balance returned by `GET /user/balance`. */
|
|
7
7
|
export interface DeepSeekBalanceLine {
|
|
@@ -46,14 +46,14 @@ export interface DeepSeekSessionSpendModel {
|
|
|
46
46
|
/** Billed cost of output tokens (reasoning included) in CNY. */
|
|
47
47
|
outputCost: number;
|
|
48
48
|
}
|
|
49
|
-
/** The billed spend of one session, priced per event by its Beijing-time
|
|
49
|
+
/** The billed spend of one session, priced per event by its Beijing-time peak/off-peak hour. */
|
|
50
50
|
export interface DeepSeekSessionSpend {
|
|
51
51
|
/** Total billed cost in CNY across every priced model. */
|
|
52
52
|
total: number;
|
|
53
53
|
/** One row per model that reported usage AND has a pricing row; empty when the session has no priced usage. */
|
|
54
54
|
models: readonly DeepSeekSessionSpendModel[];
|
|
55
55
|
}
|
|
56
|
-
/** The billed spend of every session on one Beijing-time calendar day, priced per event by its
|
|
56
|
+
/** The billed spend of every session on one Beijing-time calendar day, priced per event by its peak/off-peak hour. */
|
|
57
57
|
export interface DeepSeekTodaySpend {
|
|
58
58
|
/** Total billed cost in CNY across every priced model and every session. */
|
|
59
59
|
total: number;
|
package/lib/types/types.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-safe balance and spend vocabulary shared by the `billing` Remote,
|
|
3
3
|
* its generated artifacts, and the web UI.
|
|
4
|
-
* @module @
|
|
4
|
+
* @module @deepseek-ai/dsh-llm-billing/types
|
|
5
5
|
*/
|
|
6
6
|
export {};
|
|
7
7
|
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `billingTodaySpend` session-projection unit: per-session, per-Beijing-day
|
|
3
|
-
* billed spend, folded eagerly by the DSH projection drive over committed
|
|
4
|
-
* session events and checkpointed by the projection cache. The unit keeps only
|
|
5
|
-
* the spend of the session's LATEST priced day (events are append-only and
|
|
6
|
-
* chronological, so a day strictly older than the state's day never returns);
|
|
7
|
-
* the aggregate "today" read sums the units whose `dayKey` matches the current
|
|
8
|
-
* Beijing day — zero full-log scans once the fold is warm.
|
|
9
|
-
*
|
|
10
|
-
* The unit's fold shares {@link priceEvent} with the events-scan paths
|
|
11
|
-
* (`computeTodaySpend`), so both price with the same table. The unit is
|
|
12
|
-
* client-visible (`wire` = identity) because the persisted-cache read ladder
|
|
13
|
-
* (`sessionProjectionCache.coldSnapshot` / registry `restore`) serves only
|
|
14
|
-
* wired units; the wire value is the state itself.
|
|
15
|
-
* @module @rayadesu/dsh-llm-billing/projection
|
|
16
|
-
*/
|
|
17
|
-
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
|
|
18
|
-
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
19
|
-
import type { ResolvedBilling } from './billing.ts';
|
|
20
|
-
import type { DeepSeekTodaySpend } from './types.ts';
|
|
21
|
-
/** The projection key this unit owns. */
|
|
22
|
-
export declare const BILLING_UNIT_KEY = "billingTodaySpend";
|
|
23
|
-
/**
|
|
24
|
-
* Per-session unit state: the Beijing day of the session's latest priced
|
|
25
|
-
* event and that day's billed spend. `dayKey` is `''` while the session has no
|
|
26
|
-
* priced usage, and the state only ever describes ONE day (the latest) —
|
|
27
|
-
* plain JSON, as the persisted-cache contract requires.
|
|
28
|
-
*/
|
|
29
|
-
export interface BillingUnitState {
|
|
30
|
-
/** Beijing-time calendar-day key of the state's spend; `''` for no priced usage. */
|
|
31
|
-
dayKey: string;
|
|
32
|
-
/** The spend of the session's latest priced Beijing day. */
|
|
33
|
-
spend: DeepSeekTodaySpend;
|
|
34
|
-
}
|
|
35
|
-
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
36
|
-
interface SessionProjectionStateMap {
|
|
37
|
-
billingTodaySpend: BillingUnitState;
|
|
38
|
-
}
|
|
39
|
-
interface SessionProjectionMap {
|
|
40
|
-
billingTodaySpend: BillingUnitState;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* The unit definition with a required `wire` — the shape {@link register}
|
|
45
|
-
* accepts for a client-visible unit (the plain `ProjectionDefinition` type
|
|
46
|
-
* leaves `wire` optional).
|
|
47
|
-
*/
|
|
48
|
-
export type BillingUnitDefinition = Omit<ProjectionDefinition<'billingTodaySpend', BillingUnitState>, 'wire'> & {
|
|
49
|
-
wire: NonNullable<ProjectionDefinition<'billingTodaySpend', BillingUnitState>['wire']>;
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Build the `billingTodaySpend` unit for one resolved pricing table. The
|
|
53
|
-
* pricing closure is fixed at registration; a pricing-table change therefore
|
|
54
|
-
* prices only events folded after the change (historical spend keeps its
|
|
55
|
-
* historical rates), unlike the events-scan paths which re-price the whole
|
|
56
|
-
* log. Bump {@link ProjectionDefinition.stateVersion} whenever the state
|
|
57
|
-
* shape or fold semantics change, so persisted checkpoint rows are discarded
|
|
58
|
-
* instead of folded forward.
|
|
59
|
-
* @param billing - resolved pricing with peak-hour windows.
|
|
60
|
-
* @param catalog - model display rows, in presentation order.
|
|
61
|
-
* @returns the unit definition to register on `ctx.sessionProjections`.
|
|
62
|
-
*/
|
|
63
|
-
export declare function billingTodaySpendDefinition(billing: ResolvedBilling, catalog: readonly {
|
|
64
|
-
id: string;
|
|
65
|
-
name: string;
|
|
66
|
-
}[]): BillingUnitDefinition;
|
|
67
|
-
/** Fold a unit from init over one session's event log (the detached cold recipe). */
|
|
68
|
-
export declare function foldBillingUnit(unit: Pick<ProjectionDefinition<'billingTodaySpend', BillingUnitState>, 'init' | 'apply'>, events: readonly SessionEvent[]): BillingUnitState;
|
|
69
|
-
//# sourceMappingURL=projection.d.ts.map
|