@rayadesu/dsh-llm-billing 0.2.1 → 0.2.2
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 +594 -3169
- package/lib/invariant.js +3 -3
- package/lib/typert.host.js +31 -19
- package/lib/typert.remote-client.d.ts +3 -4
- package/lib/typert.remote-client.js +31 -19
- package/lib/types/balance.d.ts +14 -6
- package/lib/types/balance.js +9 -5
- package/lib/types/billing.d.ts +88 -7
- package/lib/types/billing.js +171 -62
- package/lib/types/index.d.ts +22 -5
- package/lib/types/index.js +64 -53
- package/lib/types/invariant.d.ts +2 -2
- package/lib/types/invariant.js +3 -3
- package/lib/types/projection.d.ts +69 -0
- package/lib/types/projection.js +88 -0
- package/lib/types/today-spend.d.ts +138 -0
- package/lib/types/today-spend.js +246 -0
- package/lib/types/types.d.ts +3 -3
- package/lib/types/types.js +1 -1
- package/package.json +20 -17
package/lib/types/billing.js
CHANGED
|
@@ -3,9 +3,19 @@
|
|
|
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
|
-
*
|
|
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).
|
|
7
18
|
*/
|
|
8
|
-
/** Published peak-hour windows (Beijing time): 09:00–12:00 and 14:00–18:00. */
|
|
9
19
|
export const DEFAULT_PEAK_HOURS = [
|
|
10
20
|
{ start: 9, end: 12 },
|
|
11
21
|
{ start: 14, end: 18 },
|
|
@@ -22,6 +32,13 @@ export const DEFAULT_MODEL_PRICING = [
|
|
|
22
32
|
peak: { cacheHitInput: 0.30, cacheMissInput: 9.0, output: 27.0 },
|
|
23
33
|
offPeak: { cacheHitInput: 0.15, cacheMissInput: 4.5, output: 13.5 },
|
|
24
34
|
},
|
|
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
|
+
},
|
|
25
42
|
];
|
|
26
43
|
/**
|
|
27
44
|
* Resolve optional configuration to a pricing table, defaulting omitted or
|
|
@@ -48,23 +65,157 @@ export function resolveBilling(config) {
|
|
|
48
65
|
function beijingHour(now) {
|
|
49
66
|
return new Date(now.getTime() + 8 * 3_600_000).getUTCHours();
|
|
50
67
|
}
|
|
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
|
+
}
|
|
51
75
|
/** The Beijing (Asia/Shanghai, UTC+8, no DST) calendar-day key of a timestamp. */
|
|
52
|
-
function beijingDayKey(now) {
|
|
76
|
+
export function beijingDayKey(now) {
|
|
53
77
|
return new Date(now.getTime() + 8 * 3_600_000).toISOString().slice(0, 10);
|
|
54
78
|
}
|
|
55
79
|
/**
|
|
56
|
-
* Whether a timestamp falls inside any peak-hour window (Beijing time
|
|
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.
|
|
57
83
|
* @param billing - resolved pricing with peak-hour windows.
|
|
58
84
|
* @param now - the moment to classify.
|
|
59
|
-
* @returns true during peak
|
|
85
|
+
* @returns true during a weekday peak hour.
|
|
60
86
|
*/
|
|
61
87
|
export function isPeak(billing, now) {
|
|
88
|
+
const weekday = beijingWeekday(now);
|
|
89
|
+
if (weekday === 0 || weekday === 6)
|
|
90
|
+
return false;
|
|
62
91
|
const hour = beijingHour(now);
|
|
63
92
|
return billing.peakHours.some(({ start, end }) => hour >= start && hour < end);
|
|
64
93
|
}
|
|
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
|
+
}
|
|
65
215
|
/**
|
|
66
216
|
* Price a set of billed events at the official per-model rates, applying the
|
|
67
|
-
* peak/off-peak table per event by its Beijing-time hour
|
|
217
|
+
* peak/off-peak table per event by its Beijing-time hour and weekday (peak
|
|
218
|
+
* windows apply Monday–Friday only; weekends are off-peak). Each
|
|
68
219
|
* `assistant/message` event with usage contributes cache-hit input, cache-miss
|
|
69
220
|
* input (uncached input plus cache writes), and output (reasoning included)
|
|
70
221
|
* tokens at the rate of its own timestamp, with the three component costs
|
|
@@ -77,64 +228,14 @@ export function isPeak(billing, now) {
|
|
|
77
228
|
*/
|
|
78
229
|
function priceEvents(events, billing, catalog) {
|
|
79
230
|
const names = new Map(catalog.map(model => [model.id, model.name]));
|
|
80
|
-
|
|
231
|
+
let spend = { total: 0, models: [] };
|
|
81
232
|
for (const event of events) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const reported = event.data.usage;
|
|
85
|
-
if (reported === undefined)
|
|
233
|
+
const priced = priceEvent(event, billing, names);
|
|
234
|
+
if (priced === undefined)
|
|
86
235
|
continue;
|
|
87
|
-
|
|
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;
|
|
236
|
+
spend = addEventContribution(spend, priced);
|
|
120
237
|
}
|
|
121
|
-
|
|
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
|
-
};
|
|
238
|
+
return spend;
|
|
138
239
|
}
|
|
139
240
|
/**
|
|
140
241
|
* Price one session's complete event log at the official per-model rates.
|
|
@@ -158,6 +259,14 @@ export function computeSessionSpend(events, billing, catalog) {
|
|
|
158
259
|
*/
|
|
159
260
|
export function computeTodaySpend(events, billing, catalog, now = new Date()) {
|
|
160
261
|
const day = beijingDayKey(now);
|
|
161
|
-
|
|
262
|
+
const names = new Map(catalog.map(model => [model.id, model.name]));
|
|
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;
|
|
162
271
|
}
|
|
163
272
|
//# sourceMappingURL=billing.js.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,15 +4,28 @@
|
|
|
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
|
-
*
|
|
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
|
|
8
17
|
*/
|
|
9
18
|
import type { Context } from '@deepseek-ai/cordis';
|
|
10
19
|
import z from '@deepseek-ai/schemastery';
|
|
11
20
|
import type { BillingConfig } from './billing.ts';
|
|
12
21
|
export { DeepSeekBalanceGateway, fetchDeepSeekBalance, parseDeepSeekBalance } from './balance.ts';
|
|
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';
|
|
22
|
+
export { addEventContribution, beijingDayKey, computeSessionSpend, computeTodaySpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyTodaySpend, isPeak, mergeTodaySpend, priceEvent, resolveBilling, } from './billing.ts';
|
|
23
|
+
export type { BillingConfig, BillingConfigModel, BillingEventContribution, DeepSeekModelPricing, DeepSeekTokenPrice, PeakHourWindow, ResolvedBilling, } from './billing.ts';
|
|
15
24
|
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';
|
|
16
29
|
export declare const name = "llm-billing";
|
|
17
30
|
/** Public API default; deployments may point elsewhere via $DEEPSEEK_BASE_URL. */
|
|
18
31
|
export declare const PUBLIC_BASE_URL = "https://api.deepseek.com";
|
|
@@ -34,12 +47,16 @@ export interface Config {
|
|
|
34
47
|
apiKeyEnv?: string;
|
|
35
48
|
/** Endpoint base; defaults to `$DEEPSEEK_BASE_URL`, then `https://api.deepseek.com`. */
|
|
36
49
|
baseURL?: string;
|
|
37
|
-
/** Advisory display rows, in presentation order; defaults to V4 Flash and V4
|
|
50
|
+
/** Advisory display rows, in presentation order; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
|
|
38
51
|
models?: BillingModel[];
|
|
39
|
-
/** Pricing table and peak-hour windows; omission uses the published defaults. */
|
|
52
|
+
/** Pricing table and peak-hour windows; omission uses the published defaults. Peak windows apply weekdays (Monday–Friday) only; weekends are always off-peak. */
|
|
40
53
|
billing?: BillingConfig;
|
|
41
54
|
}
|
|
42
55
|
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;
|
|
43
60
|
/**
|
|
44
61
|
* Register the `billing` Remote under the `billing` namespace.
|
|
45
62
|
* @param ctx - owning plugin context.
|
package/lib/types/index.js
CHANGED
|
@@ -4,16 +4,29 @@
|
|
|
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
|
-
*
|
|
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
|
|
8
17
|
*/
|
|
9
18
|
import z from '@deepseek-ai/schemastery';
|
|
10
19
|
import { assertUsableApiKey, LlmError } from '@deepseek-ai/dsh-llm';
|
|
11
20
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
12
21
|
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
|
|
13
22
|
import { DeepSeekBalanceGateway, fetchDeepSeekBalance } from "./balance.js";
|
|
14
|
-
import { computeSessionSpend,
|
|
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";
|
|
15
26
|
export { DeepSeekBalanceGateway, fetchDeepSeekBalance, parseDeepSeekBalance } from "./balance.js";
|
|
16
|
-
export { computeSessionSpend, computeTodaySpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, isPeak, resolveBilling, } from "./billing.js";
|
|
27
|
+
export { addEventContribution, beijingDayKey, computeSessionSpend, computeTodaySpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyTodaySpend, isPeak, mergeTodaySpend, priceEvent, resolveBilling, } from "./billing.js";
|
|
28
|
+
export { BILLING_UNIT_KEY, billingTodaySpendDefinition, foldBillingUnit } from "./projection.js";
|
|
29
|
+
export { TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
|
|
17
30
|
export const name = 'llm-billing';
|
|
18
31
|
const DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY';
|
|
19
32
|
const BASE_URL_ENV = 'DEEPSEEK_BASE_URL';
|
|
@@ -22,6 +35,7 @@ export const PUBLIC_BASE_URL = 'https://api.deepseek.com';
|
|
|
22
35
|
const DEFAULT_MODELS = [
|
|
23
36
|
{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash' },
|
|
24
37
|
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' },
|
|
38
|
+
{ id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp' },
|
|
25
39
|
];
|
|
26
40
|
const billingModel = z.object({
|
|
27
41
|
id: z.string().required(),
|
|
@@ -49,9 +63,14 @@ export const Config = z.object({
|
|
|
49
63
|
models: z.array(billingModel).default(DEFAULT_MODELS),
|
|
50
64
|
billing: billingConfig,
|
|
51
65
|
});
|
|
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;
|
|
52
70
|
/**
|
|
53
71
|
* Read one session's event log: the live SessionStore first, then the
|
|
54
|
-
* persistence backend for a flushed session
|
|
72
|
+
* persistence backend for a flushed session (inspected directly by id — no
|
|
73
|
+
* header listing).
|
|
55
74
|
* @param ctx - plugin context carrying the SessionStore and optional persistence.
|
|
56
75
|
* @param sessionId - the session to read.
|
|
57
76
|
* @returns the session's complete event log.
|
|
@@ -64,52 +83,14 @@ async function sessionEvents(ctx, sessionId) {
|
|
|
64
83
|
return live.events;
|
|
65
84
|
const persistence = ctx.get('sessionPersistence');
|
|
66
85
|
if (persistence !== undefined) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
continue;
|
|
70
|
-
const inspection = await persistence.inspect(sessionId);
|
|
71
|
-
return inspection.events;
|
|
86
|
+
try {
|
|
87
|
+
return (await persistence.inspect(sessionId)).events;
|
|
72
88
|
}
|
|
73
|
-
|
|
74
|
-
|
|
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);
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw new LlmError(`llm-billing: session ${sessionId} not found`, 'NOT_FOUND', { cause: error });
|
|
94
91
|
}
|
|
95
92
|
}
|
|
96
|
-
|
|
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;
|
|
93
|
+
throw new LlmError(`llm-billing: session ${sessionId} not found`, 'NOT_FOUND');
|
|
113
94
|
}
|
|
114
95
|
/**
|
|
115
96
|
* Register the `billing` Remote under the `billing` namespace.
|
|
@@ -140,16 +121,46 @@ export function apply(ctx, config) {
|
|
|
140
121
|
const apiKey = await resolveApiKey();
|
|
141
122
|
return fetchDeepSeekBalance(baseURL(), apiKey);
|
|
142
123
|
};
|
|
124
|
+
const billing = resolveBilling(config.billing);
|
|
125
|
+
const catalog = (config.models ?? DEFAULT_MODELS).map(model => ({ id: model.id, name: model.name ?? model.id }));
|
|
143
126
|
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 }));
|
|
146
127
|
return computeSessionSpend(await sessionEvents(ctx, sessionId), billing, catalog);
|
|
147
128
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
129
|
+
// Plan C: register the per-session spend projection unit on the projection
|
|
130
|
+
// registry. Registration is lazy — it happens on the first projection-path
|
|
131
|
+
// scan, not through `ctx.inject` (whose plugin-mount wait would also engage
|
|
132
|
+
// the test-invariant host in suites that never provide the registry). The
|
|
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;
|
|
152
146
|
};
|
|
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);
|
|
153
164
|
new DeepSeekBalanceGateway(ctx, { fetchBalance, fetchSessionSpend, fetchTodaySpend });
|
|
154
165
|
}
|
|
155
166
|
//# 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 `@rayadesu/dsh-llm-billing`.
|
|
3
|
+
* @module @rayadesu/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 `@rayadesu/dsh-llm-billing`.
|
|
3
|
+
* @module @rayadesu/dsh-llm-billing/invariant
|
|
4
4
|
*/
|
|
5
|
-
const PACKAGE_NAME = '@
|
|
5
|
+
const PACKAGE_NAME = '@rayadesu/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. */
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|