@rayadesu/dsh-llm-billing 0.3.7 → 0.3.9
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 +15 -11
- package/README.zh.md +15 -11
- package/lib/index.js +924 -382
- package/lib/typert.host.js +49 -5
- package/lib/typert.remote-client.d.ts +5 -3
- package/lib/typert.remote-client.js +49 -5
- package/lib/types/balance.d.ts +33 -15
- package/lib/types/balance.js +28 -13
- package/lib/types/billing.d.ts +217 -21
- package/lib/types/billing.js +445 -71
- package/lib/types/index.d.ts +14 -4
- package/lib/types/index.js +213 -70
- package/lib/types/projection.d.ts +24 -25
- package/lib/types/projection.js +30 -31
- package/lib/types/today-spend.d.ts +138 -62
- package/lib/types/today-spend.js +209 -261
- package/lib/types/types.d.ts +30 -5
- package/lib/types/types.js +8 -0
- package/package.json +5 -1
package/lib/types/index.js
CHANGED
|
@@ -25,11 +25,11 @@ import { assertUsableApiKey, LlmError } from '@deepseek-ai/dsh-llm';
|
|
|
25
25
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
26
26
|
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
|
|
27
27
|
import { DeepSeekBalanceGateway, fetchDeepSeekBalance } from "./balance.js";
|
|
28
|
-
import { computeSessionSpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, forkBoundaryOf, mergeTodaySpend, resolveBilling, } from "./billing.js";
|
|
28
|
+
import { computeSessionSpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, forkBoundaryOf, mergeTodaySpend, resolveBilling, SessionTurnSpendFolder, } from "./billing.js";
|
|
29
29
|
import { billingTodaySpendDefinition } from "./projection.js";
|
|
30
30
|
import { liveSessionEvents, persistenceInspect, TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
|
|
31
31
|
export { DeepSeekBalanceGateway, fetchDeepSeekBalance, parseDeepSeekBalance } from "./balance.js";
|
|
32
|
-
export { addEventContribution, beijingDayKey, computeSessionSpend, computeTodaySpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyTodaySpend, forkBoundaryOf, isPeak, isSeededSession, mergeTodaySpend, priceEvent, resolveBilling, SpendAccumulator, } from "./billing.js";
|
|
32
|
+
export { addEventContribution, applyBillingEvent, beijingDayKey, BillingFolder, computeSessionSpend, computeSessionTurnSpends, computeTodaySpend, computeTurnSpend, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, emptyBillingFoldState, emptyTodaySpend, forkBoundaryOf, isPeak, isSeededSession, mergeTodaySpend, negateSpend, priceEvent, priceUsage, resolveBilling, SessionTurnSpendFolder, SpendAccumulator, subtractSpend, } from "./billing.js";
|
|
33
33
|
export { BILLING_UNIT_KEY, billingTodaySpendDefinition, foldBillingUnit, foldOwnBilling } from "./projection.js";
|
|
34
34
|
export { foldSessionTitle, liveSessionEvents, persistenceInspect, persistenceListSnapshots, TodaySpendCache, TodaySpendScanner } from "./today-spend.js";
|
|
35
35
|
export const name = 'llm-billing';
|
|
@@ -39,8 +39,11 @@ const BASE_URL_ENV = 'DEEPSEEK_BASE_URL';
|
|
|
39
39
|
export const PUBLIC_BASE_URL = 'https://api.deepseek.com';
|
|
40
40
|
const DEFAULT_MODELS = [
|
|
41
41
|
{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash' },
|
|
42
|
+
{ id: 'deepseek-v4.1-flash-expires-on-0910', name: 'DeepSeek-V4.1-Flash' },
|
|
42
43
|
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' },
|
|
43
44
|
{ id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp' },
|
|
45
|
+
{ id: 'mimo-v2.5-pro', name: 'MiMo-V2.5-Pro' },
|
|
46
|
+
{ id: 'mimo-v2.5', name: 'MiMo-V2.5' },
|
|
44
47
|
];
|
|
45
48
|
const billingModel = z.object({
|
|
46
49
|
id: z.string().required(),
|
|
@@ -52,15 +55,16 @@ const tokenPrice = z.object({
|
|
|
52
55
|
output: z.number().min(0),
|
|
53
56
|
});
|
|
54
57
|
const billingConfig = z.object({
|
|
58
|
+
// Copies of the readonly published tables, taken once at module load.
|
|
55
59
|
peakHours: z.array(z.object({
|
|
56
60
|
start: z.number().step(1).min(0).max(23),
|
|
57
61
|
end: z.number().step(1).min(0).max(24),
|
|
58
|
-
})).default(DEFAULT_PEAK_HOURS),
|
|
62
|
+
})).default([...DEFAULT_PEAK_HOURS]),
|
|
59
63
|
models: z.array(z.object({
|
|
60
64
|
model: z.string().required(),
|
|
61
65
|
peak: tokenPrice,
|
|
62
66
|
offPeak: tokenPrice,
|
|
63
|
-
})).default(DEFAULT_MODEL_PRICING),
|
|
67
|
+
})).default([...DEFAULT_MODEL_PRICING]),
|
|
64
68
|
});
|
|
65
69
|
export const Config = z.object({
|
|
66
70
|
apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),
|
|
@@ -72,6 +76,27 @@ export const Config = z.object({
|
|
|
72
76
|
export const TODAY_SPEND_CACHE_MS = 60_000;
|
|
73
77
|
/** Hard cap on today's events collected by the events scan path. */
|
|
74
78
|
export const TODAY_SPEND_MAX_EVENTS = 200_000;
|
|
79
|
+
/** Max session-spend rows kept for incremental recompute before eviction. */
|
|
80
|
+
export const SESSION_SPEND_CACHE_LIMIT = 1024;
|
|
81
|
+
/** Max session-id entries kept in the per-turn-cost fold cache before eviction. */
|
|
82
|
+
export const SESSION_TURN_SPEND_CACHE_LIMIT = 64;
|
|
83
|
+
/** How long one balance snapshot is reused before the host refetches it (15s). */
|
|
84
|
+
export const BALANCE_CACHE_MS = 15_000;
|
|
85
|
+
/** Hard cap on one `/user/balance` request (5s); a hung endpoint never blocks the badge. */
|
|
86
|
+
export const BALANCE_TIMEOUT_MS = 5_000;
|
|
87
|
+
/**
|
|
88
|
+
* Bounded-map eviction: drop the oldest inserted entry once `size` reached
|
|
89
|
+
* `limit`, so an unbounded session-id space grows the map no further. Evicting
|
|
90
|
+
* one entry (instead of clearing) keeps the other sessions' incremental
|
|
91
|
+
* spend warm.
|
|
92
|
+
*/
|
|
93
|
+
function evictOldest(map, limit) {
|
|
94
|
+
if (map.size < limit)
|
|
95
|
+
return;
|
|
96
|
+
const oldest = map.keys().next().value;
|
|
97
|
+
if (oldest !== undefined)
|
|
98
|
+
map.delete(oldest);
|
|
99
|
+
}
|
|
75
100
|
/**
|
|
76
101
|
* Read one session's event log and durable seed boundary: the live
|
|
77
102
|
* SessionStore first, then the persistence backend for a flushed session
|
|
@@ -101,84 +126,111 @@ async function sessionEvents(ctx, sessionId) {
|
|
|
101
126
|
}
|
|
102
127
|
throw new LlmError(`llm-billing: session ${sessionId} not found`, 'NOT_FOUND');
|
|
103
128
|
}
|
|
129
|
+
/** Resolve the plugin's static facts once: endpoint, credential ref, pricing table. */
|
|
130
|
+
function resolveFacts(ctx, config) {
|
|
131
|
+
return {
|
|
132
|
+
baseURL: () => config.baseURL
|
|
133
|
+
?? launchEnvironmentOf(ctx).get(BASE_URL_ENV)?.value
|
|
134
|
+
?? PUBLIC_BASE_URL,
|
|
135
|
+
apiKeyRef: credentialRef(config.apiKeyEnv ?? DEFAULT_API_KEY_ENV),
|
|
136
|
+
billing: resolveBilling(config.billing),
|
|
137
|
+
catalog: (config.models ?? DEFAULT_MODELS).map(model => ({ id: model.id, name: model.name ?? model.id })),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
104
140
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* @
|
|
141
|
+
* Resolve the API key per call: the credentials service first, then the
|
|
142
|
+
* launch environment fallback.
|
|
143
|
+
* @throws {@link LlmError} with code `MISSING_CREDENTIAL` when neither yields a usable key.
|
|
108
144
|
*/
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
const ambient = launchEnvironmentOf(ctx).get(apiKeyRef);
|
|
123
|
-
if (ambient !== undefined && ambient.value.length > 0) {
|
|
124
|
-
return assertUsableApiKey(ambient.value, 'llm-billing', apiKeyRef);
|
|
125
|
-
}
|
|
145
|
+
async function resolveApiKey(ctx, apiKeyRef) {
|
|
146
|
+
const credentials = ctx.get('credentials');
|
|
147
|
+
if (credentials !== undefined) {
|
|
148
|
+
const hit = await credentials.resolve(apiKeyRef);
|
|
149
|
+
if (hit !== undefined)
|
|
150
|
+
return assertUsableApiKey(hit.value, 'llm-billing', apiKeyRef);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const ambient = launchEnvironmentOf(ctx).get(apiKeyRef);
|
|
154
|
+
if (ambient !== undefined && ambient.value.length > 0) {
|
|
155
|
+
return assertUsableApiKey(ambient.value, 'llm-billing', apiKeyRef);
|
|
126
156
|
}
|
|
127
|
-
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// session; the map is capped so an unbounded session-id space cannot grow
|
|
142
|
-
// it without bound.
|
|
157
|
+
}
|
|
158
|
+
throw new LlmError(`llm-billing: no API key; store ${apiKeyRef} through the credentials service or export it`, 'MISSING_CREDENTIAL');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Per-session incremental spend loader: a session log is append-only and
|
|
162
|
+
* chronological (the same assumption the projection unit makes), so a spend
|
|
163
|
+
* computed for `count` EVENTS OF THE SESSION'S OWN WORK (the log minus its
|
|
164
|
+
* inherited fork prefix) stays valid while the log length is unchanged, and
|
|
165
|
+
* only the appended tail needs pricing when it grows. A forked child's
|
|
166
|
+
* inherited prefix (`seq < seedLength`) is priced only in its source
|
|
167
|
+
* session; the cache is bounded (see {@link evictOldest}), so an unbounded
|
|
168
|
+
* session-id space cannot grow it without bound.
|
|
169
|
+
*/
|
|
170
|
+
function createSessionSpendFetcher(ctx, facts) {
|
|
143
171
|
const sessionSpendCache = new Map();
|
|
144
|
-
|
|
172
|
+
return async (sessionId) => {
|
|
145
173
|
const { events, seedLength } = await sessionEvents(ctx, sessionId);
|
|
146
174
|
const ownCount = events.length - seedLength;
|
|
147
175
|
const cached = sessionSpendCache.get(sessionId);
|
|
148
|
-
if (cached !== undefined && cached.count === ownCount)
|
|
176
|
+
if (cached !== undefined && cached.count === ownCount) {
|
|
177
|
+
// LRU touch: re-insert so the entry is evicted only after fresher ones.
|
|
178
|
+
sessionSpendCache.delete(sessionId);
|
|
179
|
+
sessionSpendCache.set(sessionId, cached);
|
|
149
180
|
return cached.spend;
|
|
181
|
+
}
|
|
150
182
|
if (cached !== undefined && cached.count < ownCount) {
|
|
151
|
-
const spend = mergeTodaySpend(cached.spend, computeSessionSpend(events.slice(seedLength + cached.count), billing, catalog));
|
|
183
|
+
const spend = mergeTodaySpend(cached.spend, computeSessionSpend(events.slice(seedLength + cached.count), facts.billing, facts.catalog));
|
|
184
|
+
evictOldest(sessionSpendCache, SESSION_SPEND_CACHE_LIMIT);
|
|
152
185
|
sessionSpendCache.set(sessionId, { count: ownCount, spend });
|
|
153
186
|
return spend;
|
|
154
187
|
}
|
|
155
|
-
const spend = computeSessionSpend(events, billing, catalog, seedLength);
|
|
156
|
-
|
|
157
|
-
sessionSpendCache.clear();
|
|
188
|
+
const spend = computeSessionSpend(events, facts.billing, facts.catalog, seedLength);
|
|
189
|
+
evictOldest(sessionSpendCache, SESSION_SPEND_CACHE_LIMIT);
|
|
158
190
|
sessionSpendCache.set(sessionId, { count: ownCount, spend });
|
|
159
191
|
return spend;
|
|
160
192
|
};
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Once-registrar for the billing projection unit: the first call that finds
|
|
196
|
+
* the registry composed registers the shared unit and every later call is a
|
|
197
|
+
* no-op. Registering as early as the registry exists lets DSH's projection
|
|
198
|
+
* write-behind (mandatory at `turn/end`) checkpoint a billing row for every
|
|
199
|
+
* session that runs in this process, which is what makes the zero-I/O cold
|
|
200
|
+
* path in {@link TodaySpendScanner} hit after the next restart.
|
|
201
|
+
* @param ctx - plugin context.
|
|
202
|
+
* @param unit - the unit definition built once per plugin config.
|
|
203
|
+
* @returns an idempotent registrar.
|
|
204
|
+
*/
|
|
205
|
+
function createUnitRegistrar(ctx, unit) {
|
|
206
|
+
let registered = false;
|
|
207
|
+
return () => {
|
|
208
|
+
if (registered)
|
|
172
209
|
return;
|
|
173
210
|
const registry = ctx.get('sessionProjections');
|
|
174
211
|
if (registry === undefined)
|
|
175
212
|
return;
|
|
176
213
|
registry.register(unit);
|
|
177
|
-
|
|
214
|
+
registered = true;
|
|
178
215
|
};
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Today-spend loaders over one revision-gated scanner with two 60s
|
|
219
|
+
* Beijing-day caches (in-flight coalescing and a `force` bypass):
|
|
220
|
+
* - plan C uses the per-session spend projection unit registered by the
|
|
221
|
+
* caller's {@link createUnitRegistrar} as early as the registry exists (the
|
|
222
|
+
* registry builds cells lazily over the in-memory log, so events committed
|
|
223
|
+
* before registration are folded on first touch); without the registry the
|
|
224
|
+
* events path serves today's spend.
|
|
225
|
+
* - plans A1–A3: the scanner chooses the projection path when the registry
|
|
226
|
+
* is composed, the events path otherwise.
|
|
227
|
+
* @param ctx - plugin context.
|
|
228
|
+
* @param facts - resolved endpoint, credential, pricing, and catalog facts.
|
|
229
|
+
* @param unit - the shared projection unit definition.
|
|
230
|
+
* @param ensureUnit - idempotent unit registrar (last-resort registration).
|
|
231
|
+
* @returns the two today-spend loaders.
|
|
232
|
+
*/
|
|
233
|
+
function createTodaySpendLoaders(ctx, facts, unit, ensureUnit) {
|
|
182
234
|
const scanner = new TodaySpendScanner({
|
|
183
235
|
sessions: () => ctx.get('sessions'),
|
|
184
236
|
persistence: () => ctx.get('sessionPersistence'),
|
|
@@ -188,17 +240,108 @@ export function apply(ctx, config) {
|
|
|
188
240
|
unit,
|
|
189
241
|
maxEvents: TODAY_SPEND_MAX_EVENTS,
|
|
190
242
|
logger: ctx.logger,
|
|
191
|
-
billing,
|
|
192
|
-
catalog,
|
|
243
|
+
billing: facts.billing,
|
|
244
|
+
catalog: facts.catalog,
|
|
193
245
|
});
|
|
194
|
-
const todayCache = new TodaySpendCache(dayKey => scanner.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
246
|
+
const todayCache = new TodaySpendCache(dayKey => scanner.scanDetail(dayKey), TODAY_SPEND_CACHE_MS);
|
|
247
|
+
return {
|
|
248
|
+
fetchTodaySpend: async (force = false) => (await todayCache.get(force)).aggregate,
|
|
249
|
+
fetchTodaySessionsSpend: async (force = false) => ({ sessions: (await todayCache.get(force)).sessions }),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/** One completed Turn's spend loader, located by its closing message id. */
|
|
253
|
+
function createTurnSpendFetcher(ctx, facts) {
|
|
254
|
+
return async (sessionId, messageId) => {
|
|
255
|
+
const { events } = await sessionEvents(ctx, sessionId);
|
|
256
|
+
return computeTurnSpend(events, facts.billing, facts.catalog, messageId);
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Every completed Turn's cost in one session, folded incrementally per session
|
|
261
|
+
* (session logs are append-only, so only the appended tail is priced on a
|
|
262
|
+
* growing log). One call serves a whole transcript's per-message cost rows,
|
|
263
|
+
* replacing the per-message `getTurnSpend` fan-out.
|
|
264
|
+
*/
|
|
265
|
+
function createTurnSpendsFetcher(ctx, facts) {
|
|
266
|
+
const folders = new Map();
|
|
267
|
+
return async (sessionId) => {
|
|
199
268
|
const { events } = await sessionEvents(ctx, sessionId);
|
|
200
|
-
|
|
269
|
+
let entry = folders.get(sessionId);
|
|
270
|
+
if (entry === undefined || entry.count > events.length) {
|
|
271
|
+
entry = { folder: new SessionTurnSpendFolder(facts.billing, facts.catalog), count: 0 };
|
|
272
|
+
evictOldest(folders, SESSION_TURN_SPEND_CACHE_LIMIT);
|
|
273
|
+
folders.set(sessionId, entry);
|
|
274
|
+
}
|
|
275
|
+
if (entry.count !== events.length) {
|
|
276
|
+
entry.folder.feed(events);
|
|
277
|
+
entry.count = events.length;
|
|
278
|
+
}
|
|
279
|
+
return entry.folder.finish();
|
|
201
280
|
};
|
|
202
|
-
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Balance loader with a short host-side TTL and a hard request timeout: the
|
|
284
|
+
* credential resolves per call, a fresh snapshot is reused for
|
|
285
|
+
* {@link BALANCE_CACHE_MS} (so several badge mounts and several browsers share
|
|
286
|
+
* one `/user/balance` call), concurrent misses coalesce, and `force` bypasses
|
|
287
|
+
* the TTL for the manual refresh. A hung endpoint aborts after
|
|
288
|
+
* {@link BALANCE_TIMEOUT_MS} instead of holding the badge's fetch forever.
|
|
289
|
+
* @param ctx - plugin context carrying the credential seam.
|
|
290
|
+
* @param facts - resolved endpoint and credential facts.
|
|
291
|
+
* @returns the balance loader.
|
|
292
|
+
*/
|
|
293
|
+
function createBalanceFetcher(ctx, facts) {
|
|
294
|
+
let cached;
|
|
295
|
+
let inflight;
|
|
296
|
+
return async (force = false) => {
|
|
297
|
+
if (!force && cached !== undefined && Date.now() - cached.at < BALANCE_CACHE_MS)
|
|
298
|
+
return cached.value;
|
|
299
|
+
if (inflight !== undefined)
|
|
300
|
+
return inflight;
|
|
301
|
+
const run = (async () => {
|
|
302
|
+
try {
|
|
303
|
+
const apiKey = await resolveApiKey(ctx, facts.apiKeyRef);
|
|
304
|
+
const value = await fetchDeepSeekBalance(facts.baseURL(), apiKey, AbortSignal.timeout(BALANCE_TIMEOUT_MS));
|
|
305
|
+
cached = { at: Date.now(), value };
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
inflight = undefined;
|
|
310
|
+
}
|
|
311
|
+
})();
|
|
312
|
+
inflight = run;
|
|
313
|
+
return run;
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Register the `billing` Remote under the `billing` namespace. Assembly only:
|
|
318
|
+
* facts resolve once, each loader owns its caches, and the gateway receives
|
|
319
|
+
* the bound thunks.
|
|
320
|
+
* @param ctx - owning plugin context.
|
|
321
|
+
* @param config - validated plugin config.
|
|
322
|
+
*/
|
|
323
|
+
export function apply(ctx, config) {
|
|
324
|
+
const facts = resolveFacts(ctx, config);
|
|
325
|
+
// One unit instance per plugin config, registered as early as the registry
|
|
326
|
+
// exists (and again on the first session created, in case the registry is
|
|
327
|
+
// composed after this plugin): DSH's write-behind then checkpoints a
|
|
328
|
+
// billing row for every session that runs in this process.
|
|
329
|
+
const unit = billingTodaySpendDefinition(facts.billing, facts.catalog);
|
|
330
|
+
const ensureUnit = createUnitRegistrar(ctx, unit);
|
|
331
|
+
ensureUnit();
|
|
332
|
+
ctx.on('session/created', ensureUnit);
|
|
333
|
+
const fetchBalance = createBalanceFetcher(ctx, facts);
|
|
334
|
+
const fetchSessionSpend = createSessionSpendFetcher(ctx, facts);
|
|
335
|
+
const { fetchTodaySpend, fetchTodaySessionsSpend } = createTodaySpendLoaders(ctx, facts, unit, ensureUnit);
|
|
336
|
+
const fetchTurnSpend = createTurnSpendFetcher(ctx, facts);
|
|
337
|
+
const fetchTurnSpends = createTurnSpendsFetcher(ctx, facts);
|
|
338
|
+
new DeepSeekBalanceGateway(ctx, {
|
|
339
|
+
fetchBalance,
|
|
340
|
+
fetchSessionSpend,
|
|
341
|
+
fetchTodaySpend,
|
|
342
|
+
fetchTodaySessionsSpend,
|
|
343
|
+
fetchTurnSpend,
|
|
344
|
+
fetchTurnSpends,
|
|
345
|
+
});
|
|
203
346
|
}
|
|
204
347
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,37 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `billingTodaySpend` session-projection unit: per-session,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the aggregate "today" read sums the units whose `dayKey`
|
|
8
|
-
* Beijing day — zero full-log scans once the fold is warm.
|
|
2
|
+
* `billingTodaySpend` session-projection unit: per-session billed spend,
|
|
3
|
+
* folded eagerly by the DSH projection drive over committed session events and
|
|
4
|
+
* checkpointed by the projection cache. The state keeps the session's LATEST
|
|
5
|
+
* priced Beijing day, its whole-session total, the fork boundary, the latest
|
|
6
|
+
* request model, and the last priced attempt sample (DSH's same-step
|
|
7
|
+
* replacement rule); the aggregate "today" read sums the units whose `dayKey`
|
|
8
|
+
* matches the current Beijing day — zero full-log scans once the fold is warm.
|
|
9
9
|
*
|
|
10
|
-
* The unit's fold
|
|
11
|
-
*
|
|
10
|
+
* The unit's fold IS the shared pricing fold ({@link applyBillingEvent}), so
|
|
11
|
+
* the projection path and the events-scan paths cannot drift. The unit is
|
|
12
12
|
* client-visible (`wire` = identity) because the persisted-cache read ladder
|
|
13
|
-
* (`sessionProjectionCache.
|
|
14
|
-
* wired units
|
|
13
|
+
* (`sessionProjectionCache.cachedSnapshot` / registry `restore`) serves only
|
|
14
|
+
* wired units, and because the browser half reads this value through
|
|
15
|
+
* `useProjection` instead of polling a Remote; the wire value is the state
|
|
16
|
+
* itself.
|
|
15
17
|
* @module @rayadesu/dsh-llm-billing/projection
|
|
16
18
|
*/
|
|
17
19
|
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
|
|
18
20
|
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
19
|
-
import type { ResolvedBilling } from './billing.ts';
|
|
20
|
-
import type { DeepSeekTodaySpend } from './types.ts';
|
|
21
|
+
import type { BillingFoldState, ResolvedBilling } from './billing.ts';
|
|
21
22
|
/** The projection key this unit owns. */
|
|
22
23
|
export declare const BILLING_UNIT_KEY = "billingTodaySpend";
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* The unit's state is the shared billing fold state: the latest priced
|
|
26
|
+
* Beijing day, the whole-session total, the fork boundary, the latest request
|
|
27
|
+
* model, and the last priced attempt sample. Plain JSON, as the
|
|
28
|
+
* persisted-cache contract requires. The fold is boundary-aware: it prices
|
|
29
|
+
* only the session's OWN events (a fork child's inherited prefix is skipped),
|
|
30
|
+
* so both totals match the Remote paths and the client can read them without
|
|
31
|
+
* a Remote call.
|
|
28
32
|
*/
|
|
29
|
-
export
|
|
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
|
-
}
|
|
33
|
+
export type BillingUnitState = BillingFoldState;
|
|
35
34
|
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
36
35
|
interface SessionProjectionStateMap {
|
|
37
36
|
billingTodaySpend: BillingUnitState;
|
|
@@ -69,8 +68,8 @@ export interface BillingUnitFold {
|
|
|
69
68
|
/**
|
|
70
69
|
* Initial state for the empty log. DSH ≤ 0.1.1-rc.2 declared `init()` with
|
|
71
70
|
* no parameters; 0.1.2-alpha.5+ passes the Session header and inherited
|
|
72
|
-
* count.
|
|
73
|
-
*
|
|
71
|
+
* count. Detached folds call it with no arguments and apply the boundary
|
|
72
|
+
* themselves (see {@link foldOwnBilling}), so both call shapes stay valid.
|
|
74
73
|
*/
|
|
75
74
|
init(...metadata: never[]): BillingUnitState;
|
|
76
75
|
/** Pure transition: previous state + one committed event → next state. */
|
package/lib/types/projection.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `billingTodaySpend` session-projection unit: per-session,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the aggregate "today" read sums the units whose `dayKey`
|
|
8
|
-
* Beijing day — zero full-log scans once the fold is warm.
|
|
2
|
+
* `billingTodaySpend` session-projection unit: per-session billed spend,
|
|
3
|
+
* folded eagerly by the DSH projection drive over committed session events and
|
|
4
|
+
* checkpointed by the projection cache. The state keeps the session's LATEST
|
|
5
|
+
* priced Beijing day, its whole-session total, the fork boundary, the latest
|
|
6
|
+
* request model, and the last priced attempt sample (DSH's same-step
|
|
7
|
+
* replacement rule); the aggregate "today" read sums the units whose `dayKey`
|
|
8
|
+
* matches the current Beijing day — zero full-log scans once the fold is warm.
|
|
9
9
|
*
|
|
10
|
-
* The unit's fold
|
|
11
|
-
*
|
|
10
|
+
* The unit's fold IS the shared pricing fold ({@link applyBillingEvent}), so
|
|
11
|
+
* the projection path and the events-scan paths cannot drift. The unit is
|
|
12
12
|
* client-visible (`wire` = identity) because the persisted-cache read ladder
|
|
13
|
-
* (`sessionProjectionCache.
|
|
14
|
-
* wired units
|
|
13
|
+
* (`sessionProjectionCache.cachedSnapshot` / registry `restore`) serves only
|
|
14
|
+
* wired units, and because the browser half reads this value through
|
|
15
|
+
* `useProjection` instead of polling a Remote; the wire value is the state
|
|
16
|
+
* itself.
|
|
15
17
|
* @module @rayadesu/dsh-llm-billing/projection
|
|
16
18
|
*/
|
|
17
19
|
import { z } from 'zod';
|
|
18
|
-
import {
|
|
20
|
+
import { applyBillingEvent, emptyBillingFoldState } from "./billing.js";
|
|
19
21
|
/** The projection key this unit owns. */
|
|
20
22
|
export const BILLING_UNIT_KEY = 'billingTodaySpend';
|
|
21
23
|
const modelRowSchema = z.object({
|
|
@@ -38,6 +40,15 @@ const todaySpendSchema = z.object({
|
|
|
38
40
|
const billingUnitSchema = z.object({
|
|
39
41
|
dayKey: z.string(),
|
|
40
42
|
spend: todaySpendSchema,
|
|
43
|
+
session: todaySpendSchema,
|
|
44
|
+
inheritedEventCount: z.number().int().nonnegative(),
|
|
45
|
+
model: z.string(),
|
|
46
|
+
last: z.object({
|
|
47
|
+
turn: z.number().int().nonnegative(),
|
|
48
|
+
step: z.number().int().nonnegative(),
|
|
49
|
+
dayKey: z.string(),
|
|
50
|
+
spend: todaySpendSchema,
|
|
51
|
+
}).strict().nullable(),
|
|
41
52
|
}).strict();
|
|
42
53
|
/**
|
|
43
54
|
* Build the `billingTodaySpend` unit for one resolved pricing table. The
|
|
@@ -55,26 +66,14 @@ export function billingTodaySpendDefinition(billing, catalog) {
|
|
|
55
66
|
const names = new Map(catalog.map(model => [model.id, model.name]));
|
|
56
67
|
return {
|
|
57
68
|
key: BILLING_UNIT_KEY,
|
|
58
|
-
|
|
69
|
+
// v3: DSH-aligned attempt pricing (assistant/attempt samples, same-step
|
|
70
|
+
// replacement, `llm/retry-started` closes the slot) on top of v2's
|
|
71
|
+
// boundary-aware fold and whole-session total; older checkpoint rows are
|
|
72
|
+
// discarded and refolded.
|
|
73
|
+
stateVersion: 3,
|
|
59
74
|
stateSchema: billingUnitSchema,
|
|
60
|
-
init: () => (
|
|
61
|
-
apply: (state, event) =>
|
|
62
|
-
const priced = priceEvent(event, billing, names);
|
|
63
|
-
if (priced === undefined)
|
|
64
|
-
return state;
|
|
65
|
-
if (state.dayKey === priced.dayKey) {
|
|
66
|
-
return { dayKey: state.dayKey, spend: addEventContribution(state.spend, priced) };
|
|
67
|
-
}
|
|
68
|
-
// The session log is append-only and chronological, so an event whose
|
|
69
|
-
// Beijing day is strictly older than the state's day cannot legally
|
|
70
|
-
// follow it; ignore defensively to keep the persisted fold
|
|
71
|
-
// deterministic under reordered or clock-skewed timestamps.
|
|
72
|
-
if (state.dayKey !== '' && priced.dayKey < state.dayKey)
|
|
73
|
-
return state;
|
|
74
|
-
// First priced event, or the session's first priced event of a new day:
|
|
75
|
-
// the state resets to that day's spend.
|
|
76
|
-
return { dayKey: priced.dayKey, spend: addEventContribution(emptyTodaySpend(), priced) };
|
|
77
|
-
},
|
|
75
|
+
init: (_header, inheritedEventCount) => emptyBillingFoldState(Number(inheritedEventCount ?? 0)),
|
|
76
|
+
apply: (state, event) => applyBillingEvent(state, event, billing, names),
|
|
78
77
|
wire: { viewSchema: billingUnitSchema, view: state => state },
|
|
79
78
|
};
|
|
80
79
|
}
|