@cassius0924/dsh-usage-dashboard 0.3.1 → 0.5.0
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.md +7 -1
- package/lib/client.js +2473 -648
- package/lib/client.js.map +4 -4
- package/lib/index.js +252 -92
- package/lib/index.js.map +3 -3
- package/lib/types/client/api.d.ts +4 -1
- package/lib/types/client/boundary.d.ts +2 -0
- package/lib/types/client/budget.d.ts +16 -0
- package/lib/types/client/chart-focus.d.ts +35 -0
- package/lib/types/client/charts.d.ts +23 -5
- package/lib/types/client/context.d.ts +13 -0
- package/lib/types/client/dashboard.d.ts +7 -2
- package/lib/types/client/dom.d.ts +28 -0
- package/lib/types/client/export.d.ts +9 -0
- package/lib/types/client/freshness.d.ts +5 -0
- package/lib/types/client/i18n.d.ts +15 -0
- package/lib/types/client/locales.d.ts +253 -0
- package/lib/types/client/message-cost.d.ts +10 -0
- package/lib/types/client/metric.d.ts +12 -0
- package/lib/types/client/store.d.ts +11 -0
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/client/views.d.ts +23 -0
- package/lib/types/client/widget.d.ts +10 -1
- package/lib/types/context.d.ts +7 -0
- package/lib/types/contract.d.ts +104 -0
- package/lib/types/index.d.ts +9 -6
- package/lib/types/usage.d.ts +10 -10
- package/package.json +4 -2
package/lib/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/contract.ts
|
|
2
|
+
var USAGE_WINDOW_DAYS = [7, 30, 90, 365];
|
|
3
|
+
var SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
4
|
+
var isValidSessionId = (id) => SESSION_ID_PATTERN.test(id);
|
|
5
|
+
|
|
1
6
|
// src/memo.ts
|
|
2
7
|
function memoize(ttlMs, compute) {
|
|
3
8
|
const state = { entry: void 0, inflight: void 0 };
|
|
@@ -159,6 +164,17 @@ async function fetchBalance(credentials) {
|
|
|
159
164
|
};
|
|
160
165
|
}
|
|
161
166
|
var emptyBucket = () => ({ input: 0, output: 0, cache: 0, total: 0, cost: 0, calls: 0 });
|
|
167
|
+
function bump(map, key, input, output, cache, cost) {
|
|
168
|
+
const bucket = map.get(key) ?? emptyBucket();
|
|
169
|
+
bucket.input += input;
|
|
170
|
+
bucket.output += output;
|
|
171
|
+
bucket.cache += cache;
|
|
172
|
+
bucket.total += input + output + cache;
|
|
173
|
+
bucket.cost += cost;
|
|
174
|
+
bucket.calls += 1;
|
|
175
|
+
map.set(key, bucket);
|
|
176
|
+
}
|
|
177
|
+
var emptyOverall = () => ({ ...emptyBucket(), reasoning: 0, cacheSavings: 0 });
|
|
162
178
|
var SESSION_TOP_N = 6;
|
|
163
179
|
function titleOf(events, id) {
|
|
164
180
|
let title = "";
|
|
@@ -197,7 +213,8 @@ function summarize(dayMap, now) {
|
|
|
197
213
|
};
|
|
198
214
|
}
|
|
199
215
|
function addUsageEvent(events, onEvent) {
|
|
200
|
-
|
|
216
|
+
const result = { usageRecords: 0, skippedRecords: 0 };
|
|
217
|
+
if (events === void 0) return result;
|
|
201
218
|
let provider = "";
|
|
202
219
|
let model = "";
|
|
203
220
|
for (const ev of events) {
|
|
@@ -212,15 +229,22 @@ function addUsageEvent(events, onEvent) {
|
|
|
212
229
|
if (ev?.type !== "assistant/message") continue;
|
|
213
230
|
const usage = ev.data?.usage;
|
|
214
231
|
if (usage === void 0) continue;
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
232
|
+
const values = [usage.inputTokens, usage.outputTokens, usage.cacheReadTokens, usage.reasoningTokens].map((value) => value === void 0 ? 0 : Number(value));
|
|
233
|
+
if (typeof ev.time !== "number" || !Number.isFinite(ev.time) || values.some((value) => !Number.isFinite(value) || value < 0)) {
|
|
234
|
+
result.skippedRecords += 1;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const [input = 0, output = 0, cache = 0, reasoning = 0] = values;
|
|
238
|
+
const rawMessageId = ev.data?.message?.id;
|
|
239
|
+
const messageId = typeof rawMessageId === "string" && rawMessageId !== "" ? rawMessageId : void 0;
|
|
240
|
+
const rawTurn = ev.data?.turn;
|
|
241
|
+
const turn = typeof rawTurn === "number" && Number.isFinite(rawTurn) ? rawTurn : void 0;
|
|
242
|
+
onEvent(ev.time, input, output, cache, reasoning, provider, model, messageId, turn);
|
|
243
|
+
result.usageRecords += 1;
|
|
221
244
|
}
|
|
245
|
+
return result;
|
|
222
246
|
}
|
|
223
|
-
async function fetchUsage(persistence) {
|
|
247
|
+
async function fetchUsage(persistence, nowMs = Date.now()) {
|
|
224
248
|
if (persistence === void 0) return { ok: false, error: "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528" };
|
|
225
249
|
let headers;
|
|
226
250
|
try {
|
|
@@ -230,132 +254,189 @@ async function fetchUsage(persistence) {
|
|
|
230
254
|
}
|
|
231
255
|
const dayMap = /* @__PURE__ */ new Map();
|
|
232
256
|
const hourMap = /* @__PURE__ */ new Map();
|
|
233
|
-
const overall =
|
|
257
|
+
const overall = emptyOverall();
|
|
234
258
|
const modelTotals = /* @__PURE__ */ new Map();
|
|
235
259
|
const modelDays = /* @__PURE__ */ new Map();
|
|
236
260
|
const modelHours = /* @__PURE__ */ new Map();
|
|
237
261
|
const sessions = [];
|
|
262
|
+
const coverage = {
|
|
263
|
+
scope: "local-dsh-session-logs",
|
|
264
|
+
listedSessions: 0,
|
|
265
|
+
scannedSessions: 0,
|
|
266
|
+
usageRecords: 0,
|
|
267
|
+
skippedRecords: 0,
|
|
268
|
+
failedSessions: 0,
|
|
269
|
+
earliestAt: null,
|
|
270
|
+
latestAt: null
|
|
271
|
+
};
|
|
238
272
|
const peakSplit = {
|
|
239
273
|
peak: { total: 0, cost: 0, calls: 0 },
|
|
240
274
|
offPeak: { total: 0, cost: 0, calls: 0 },
|
|
241
275
|
peakEraCost: 0,
|
|
242
276
|
offPeakEraCost: 0
|
|
243
277
|
};
|
|
244
|
-
const
|
|
245
|
-
|
|
278
|
+
const now = new Date(nowMs);
|
|
279
|
+
const dateBefore = (days) => new Date(now.getFullYear(), now.getMonth(), now.getDate() - days);
|
|
280
|
+
const windowAggregates = USAGE_WINDOW_DAYS.map((days) => ({
|
|
281
|
+
days,
|
|
282
|
+
startKey: dayKeyOf(dateBefore(days - 1)),
|
|
283
|
+
endKey: dayKeyOf(now),
|
|
284
|
+
dayMap: /* @__PURE__ */ new Map(),
|
|
285
|
+
hourMap: /* @__PURE__ */ new Map(),
|
|
286
|
+
overall: emptyOverall(),
|
|
287
|
+
modelTotals: /* @__PURE__ */ new Map(),
|
|
288
|
+
modelDays: /* @__PURE__ */ new Map(),
|
|
289
|
+
modelHours: /* @__PURE__ */ new Map(),
|
|
290
|
+
sessions: []
|
|
291
|
+
}));
|
|
292
|
+
const bumpOverall = (bucket, input, output, cache, reasoning, cost, cacheSavings) => {
|
|
246
293
|
bucket.input += input;
|
|
247
294
|
bucket.output += output;
|
|
248
295
|
bucket.cache += cache;
|
|
296
|
+
bucket.reasoning += reasoning;
|
|
249
297
|
bucket.total += input + output + cache;
|
|
250
298
|
bucket.cost += cost;
|
|
299
|
+
bucket.cacheSavings += cacheSavings;
|
|
251
300
|
bucket.calls += 1;
|
|
252
|
-
map.set(key, bucket);
|
|
253
301
|
};
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
302
|
+
const bumpModel = (totals, days, hours, modelKey, dayKey, hourKey, input, output, cache, cost) => {
|
|
303
|
+
bump(totals, modelKey, input, output, cache, cost);
|
|
304
|
+
let dayBuckets = days.get(modelKey);
|
|
305
|
+
if (dayBuckets === void 0) {
|
|
306
|
+
dayBuckets = /* @__PURE__ */ new Map();
|
|
307
|
+
days.set(modelKey, dayBuckets);
|
|
308
|
+
}
|
|
309
|
+
bump(dayBuckets, dayKey, input, output, cache, cost);
|
|
310
|
+
let hourBuckets = hours.get(modelKey);
|
|
311
|
+
if (hourBuckets === void 0) {
|
|
312
|
+
hourBuckets = /* @__PURE__ */ new Map();
|
|
313
|
+
hours.set(modelKey, hourBuckets);
|
|
314
|
+
}
|
|
315
|
+
bump(hourBuckets, hourKey, input, output, cache, cost);
|
|
316
|
+
};
|
|
317
|
+
const bumpSession = (session, time, total, cost) => {
|
|
318
|
+
session.total += total;
|
|
319
|
+
session.cost += cost;
|
|
320
|
+
session.calls += 1;
|
|
321
|
+
if (time > session.lastActive) session.lastActive = time;
|
|
322
|
+
};
|
|
323
|
+
const sessionIds = headers.map((header) => header.id ?? header.sessionId).filter((sid) => sid !== void 0 && sid !== "");
|
|
324
|
+
coverage.listedSessions = sessionIds.length;
|
|
325
|
+
for (const sid of sessionIds) {
|
|
257
326
|
try {
|
|
258
327
|
const { events } = await persistence.readFrom(sid, 0);
|
|
328
|
+
coverage.scannedSessions += 1;
|
|
259
329
|
const session = { id: sid, title: "", total: 0, cost: 0, calls: 0, lastActive: 0 };
|
|
260
|
-
|
|
330
|
+
const windowSessions = windowAggregates.map(() => ({ id: sid, title: "", total: 0, cost: 0, calls: 0, lastActive: 0 }));
|
|
331
|
+
const scanned = addUsageEvent(events, (time, input, output, cache, reasoning, provider, model) => {
|
|
332
|
+
if (coverage.earliestAt === null || time < coverage.earliestAt) coverage.earliestAt = time;
|
|
333
|
+
if (coverage.latestAt === null || time > coverage.latestAt) coverage.latestAt = time;
|
|
261
334
|
const d = new Date(time);
|
|
262
335
|
const dayKey = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
263
336
|
const hourKey = String(d.getHours());
|
|
264
337
|
const modelKey = `${provider}/${model}`;
|
|
265
338
|
const cost = costOf(time, model, input, cache, output);
|
|
339
|
+
const cacheSavings = cacheSavingOf(time, model, cache);
|
|
340
|
+
const eventTotal = input + output + cache;
|
|
266
341
|
bump(dayMap, dayKey, input, output, cache, cost);
|
|
267
342
|
bump(hourMap, hourKey, input, output, cache, cost);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
343
|
+
bumpModel(modelTotals, modelDays, modelHours, modelKey, dayKey, hourKey, input, output, cache, cost);
|
|
344
|
+
bumpOverall(overall, input, output, cache, reasoning, cost, cacheSavings);
|
|
345
|
+
bumpSession(session, time, eventTotal, cost);
|
|
346
|
+
for (let i = 0; i < windowAggregates.length; i++) {
|
|
347
|
+
const aggregate = windowAggregates[i];
|
|
348
|
+
if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue;
|
|
349
|
+
bump(aggregate.dayMap, dayKey, input, output, cache, cost);
|
|
350
|
+
bump(aggregate.hourMap, hourKey, input, output, cache, cost);
|
|
351
|
+
bumpModel(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, modelKey, dayKey, hourKey, input, output, cache, cost);
|
|
352
|
+
bumpOverall(aggregate.overall, input, output, cache, reasoning, cost, cacheSavings);
|
|
353
|
+
bumpSession(windowSessions[i], time, eventTotal, cost);
|
|
279
354
|
}
|
|
280
|
-
bump(hours, hourKey, input, output, cache, cost);
|
|
281
|
-
overall.input += input;
|
|
282
|
-
overall.output += output;
|
|
283
|
-
overall.cache += cache;
|
|
284
|
-
overall.reasoning += reasoning;
|
|
285
|
-
overall.total += input + output + cache;
|
|
286
|
-
overall.cost += cost;
|
|
287
|
-
overall.cacheSavings += cacheSavingOf(time, model, cache);
|
|
288
|
-
session.total += input + output + cache;
|
|
289
|
-
session.cost += cost;
|
|
290
|
-
session.calls += 1;
|
|
291
|
-
if (time > session.lastActive) session.lastActive = time;
|
|
292
355
|
const window = isPeak(time) ? peakSplit.peak : peakSplit.offPeak;
|
|
293
|
-
window.total +=
|
|
356
|
+
window.total += eventTotal;
|
|
294
357
|
window.cost += cost;
|
|
295
358
|
window.calls += 1;
|
|
296
359
|
peakSplit.peakEraCost += costUnderPeakEra(time, model, input, cache, output);
|
|
297
360
|
peakSplit.offPeakEraCost += costUnderPeakEra(time, model, input, cache, output, true);
|
|
298
|
-
overall.calls += 1;
|
|
299
361
|
});
|
|
362
|
+
coverage.usageRecords += scanned.usageRecords;
|
|
363
|
+
coverage.skippedRecords += scanned.skippedRecords;
|
|
300
364
|
if (session.calls > 0) {
|
|
301
|
-
|
|
365
|
+
const title = titleOf(events, sid);
|
|
366
|
+
session.title = title;
|
|
302
367
|
sessions.push(session);
|
|
368
|
+
for (let i = 0; i < windowAggregates.length; i++) {
|
|
369
|
+
const windowSession = windowSessions[i];
|
|
370
|
+
if (windowSession.calls === 0) continue;
|
|
371
|
+
windowSession.title = title;
|
|
372
|
+
windowAggregates[i].sessions.push(windowSession);
|
|
373
|
+
}
|
|
303
374
|
}
|
|
304
375
|
} catch {
|
|
376
|
+
coverage.failedSessions += 1;
|
|
305
377
|
}
|
|
306
378
|
}
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
const hourly = [];
|
|
315
|
-
for (let i = 0; i < 24; i++) {
|
|
316
|
-
const b = hourMap.get(String(i)) ?? emptyBucket();
|
|
317
|
-
hourly.push({ hour: i, total: b.total, cost: b.cost, calls: b.calls });
|
|
318
|
-
}
|
|
319
|
-
const heatmap = [];
|
|
320
|
-
for (let i = 83; i >= 0; i--) {
|
|
321
|
-
const d = new Date(Date.now() - i * 864e5);
|
|
322
|
-
const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
323
|
-
const b = dayMap.get(key) ?? emptyBucket();
|
|
324
|
-
heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls });
|
|
325
|
-
}
|
|
326
|
-
const models = [];
|
|
327
|
-
for (const [modelKey, bucket] of modelTotals) {
|
|
328
|
-
const slash = modelKey.indexOf("/");
|
|
329
|
-
const provider = slash < 0 ? "" : modelKey.slice(0, slash);
|
|
330
|
-
const model = slash < 0 ? modelKey : modelKey.slice(slash + 1);
|
|
331
|
-
const days = modelDays.get(modelKey);
|
|
332
|
-
const hours = modelHours.get(modelKey);
|
|
333
|
-
const daily2 = [];
|
|
334
|
-
for (let i = 29; i >= 0; i--) {
|
|
335
|
-
const d = new Date(Date.now() - i * 864e5);
|
|
336
|
-
const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
337
|
-
const b = days?.get(key) ?? emptyBucket();
|
|
338
|
-
daily2.push({ total: b.total, cost: b.cost, calls: b.calls });
|
|
379
|
+
const makeDaily = (map, count) => {
|
|
380
|
+
const result = [];
|
|
381
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
382
|
+
const key = dayKeyOf(dateBefore(i));
|
|
383
|
+
const b = map.get(key) ?? emptyBucket();
|
|
384
|
+
result.push({ date: key, input: b.input, output: b.output, cache: b.cache, total: b.total, cost: b.cost, calls: b.calls });
|
|
339
385
|
}
|
|
340
|
-
|
|
386
|
+
return result;
|
|
387
|
+
};
|
|
388
|
+
const makeHourly = (map) => {
|
|
389
|
+
const result = [];
|
|
341
390
|
for (let i = 0; i < 24; i++) {
|
|
342
|
-
const b =
|
|
343
|
-
|
|
391
|
+
const b = map.get(String(i)) ?? emptyBucket();
|
|
392
|
+
result.push({ hour: i, total: b.total, cost: b.cost, calls: b.calls });
|
|
344
393
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
394
|
+
return result;
|
|
395
|
+
};
|
|
396
|
+
const makeModels = (totals, daysByModel, hoursByModel, dayCount) => {
|
|
397
|
+
const result = [];
|
|
398
|
+
for (const [modelKey, bucket] of totals) {
|
|
399
|
+
const slash = modelKey.indexOf("/");
|
|
400
|
+
const provider = slash < 0 ? "" : modelKey.slice(0, slash);
|
|
401
|
+
const model = slash < 0 ? modelKey : modelKey.slice(slash + 1);
|
|
402
|
+
const days = daysByModel.get(modelKey);
|
|
403
|
+
const hours = hoursByModel.get(modelKey);
|
|
404
|
+
const daily2 = makeDaily(days ?? /* @__PURE__ */ new Map(), dayCount).map((point) => ({ total: point.total, cost: point.cost, calls: point.calls }));
|
|
405
|
+
const hourly2 = makeHourly(hours ?? /* @__PURE__ */ new Map()).map((point) => ({ total: point.total, cost: point.cost, calls: point.calls }));
|
|
406
|
+
result.push({
|
|
407
|
+
provider,
|
|
408
|
+
model,
|
|
409
|
+
input: bucket.input,
|
|
410
|
+
output: bucket.output,
|
|
411
|
+
cache: bucket.cache,
|
|
412
|
+
total: bucket.total,
|
|
413
|
+
cost: bucket.cost,
|
|
414
|
+
calls: bucket.calls,
|
|
415
|
+
daily: daily2,
|
|
416
|
+
hourly: hourly2
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
result.sort((a, b) => b.total - a.total);
|
|
420
|
+
return result;
|
|
421
|
+
};
|
|
422
|
+
const daily = makeDaily(dayMap, 30);
|
|
423
|
+
const hourly = makeHourly(hourMap);
|
|
424
|
+
const heatmap = [];
|
|
425
|
+
for (let i = 363; i >= 0; i--) {
|
|
426
|
+
const key = dayKeyOf(dateBefore(i));
|
|
427
|
+
const b = dayMap.get(key) ?? emptyBucket();
|
|
428
|
+
heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls });
|
|
357
429
|
}
|
|
358
|
-
models
|
|
430
|
+
const models = makeModels(modelTotals, modelDays, modelHours, 30);
|
|
431
|
+
const windows = windowAggregates.map((aggregate) => ({
|
|
432
|
+
days: aggregate.days,
|
|
433
|
+
daily: makeDaily(aggregate.dayMap, aggregate.days),
|
|
434
|
+
hourly: makeHourly(aggregate.hourMap),
|
|
435
|
+
totals: { ...aggregate.overall },
|
|
436
|
+
models: makeModels(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, aggregate.days),
|
|
437
|
+
sessions: [...aggregate.sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),
|
|
438
|
+
sessionCount: aggregate.sessions.length
|
|
439
|
+
}));
|
|
359
440
|
return {
|
|
360
441
|
ok: true,
|
|
361
442
|
data: {
|
|
@@ -363,11 +444,13 @@ async function fetchUsage(persistence) {
|
|
|
363
444
|
hourly,
|
|
364
445
|
heatmap,
|
|
365
446
|
models,
|
|
366
|
-
summary: summarize(dayMap,
|
|
367
|
-
pricing: pricingInfo(
|
|
447
|
+
summary: summarize(dayMap, new Date(nowMs)),
|
|
448
|
+
pricing: pricingInfo(nowMs),
|
|
368
449
|
peakSplit,
|
|
369
450
|
sessions: [...sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),
|
|
370
451
|
sessionCount: sessions.length,
|
|
452
|
+
coverage,
|
|
453
|
+
windows,
|
|
371
454
|
totals: {
|
|
372
455
|
input: overall.input,
|
|
373
456
|
output: overall.output,
|
|
@@ -381,6 +464,74 @@ async function fetchUsage(persistence) {
|
|
|
381
464
|
}
|
|
382
465
|
};
|
|
383
466
|
}
|
|
467
|
+
async function fetchSessionUsage(persistence, sessionId) {
|
|
468
|
+
if (persistence === void 0) return { ok: false, error: "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528" };
|
|
469
|
+
if (typeof sessionId !== "string" || sessionId === "") return { ok: false, error: "\u7F3A\u5C11\u4F1A\u8BDD id" };
|
|
470
|
+
if (!isValidSessionId(sessionId)) return { ok: false, error: "\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5" };
|
|
471
|
+
let events;
|
|
472
|
+
try {
|
|
473
|
+
const result = await persistence.readFrom(sessionId, 0);
|
|
474
|
+
events = result.events;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A${errorMessage(err)}` };
|
|
477
|
+
}
|
|
478
|
+
const modelTotals = /* @__PURE__ */ new Map();
|
|
479
|
+
let total = 0;
|
|
480
|
+
let cost = 0;
|
|
481
|
+
let calls = 0;
|
|
482
|
+
let firstActive = null;
|
|
483
|
+
let lastActive = null;
|
|
484
|
+
const turnTotals = /* @__PURE__ */ new Map();
|
|
485
|
+
addUsageEvent(events, (time, input, output, cache, _reasoning, provider, model, messageId, turn) => {
|
|
486
|
+
const modelKey = `${provider}/${model}`;
|
|
487
|
+
const eventCost = costOf(time, model, input, cache, output);
|
|
488
|
+
const eventTotal = input + output + cache;
|
|
489
|
+
bump(modelTotals, modelKey, input, output, cache, eventCost);
|
|
490
|
+
total += eventTotal;
|
|
491
|
+
cost += eventCost;
|
|
492
|
+
calls += 1;
|
|
493
|
+
if (firstActive === null || time < firstActive) firstActive = time;
|
|
494
|
+
if (lastActive === null || time > lastActive) lastActive = time;
|
|
495
|
+
if (messageId !== void 0) {
|
|
496
|
+
const key = turn === void 0 ? `message:${messageId}` : `turn:${turn}`;
|
|
497
|
+
const aggregate = turnTotals.get(key) ?? { messageId, total: 0, cost: 0, calls: 0 };
|
|
498
|
+
aggregate.messageId = messageId;
|
|
499
|
+
aggregate.total += eventTotal;
|
|
500
|
+
aggregate.cost += eventCost;
|
|
501
|
+
aggregate.calls += 1;
|
|
502
|
+
turnTotals.set(key, aggregate);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
const models = [...modelTotals.entries()].map(([modelKey, bucket]) => {
|
|
506
|
+
const slash = modelKey.indexOf("/");
|
|
507
|
+
const provider = slash < 0 ? "" : modelKey.slice(0, slash);
|
|
508
|
+
const model = slash < 0 ? modelKey : modelKey.slice(slash + 1);
|
|
509
|
+
return {
|
|
510
|
+
provider,
|
|
511
|
+
model,
|
|
512
|
+
input: bucket.input,
|
|
513
|
+
output: bucket.output,
|
|
514
|
+
cache: bucket.cache,
|
|
515
|
+
total: bucket.total,
|
|
516
|
+
cost: bucket.cost,
|
|
517
|
+
calls: bucket.calls
|
|
518
|
+
};
|
|
519
|
+
}).sort((a, b) => b.cost - a.cost);
|
|
520
|
+
return {
|
|
521
|
+
ok: true,
|
|
522
|
+
data: {
|
|
523
|
+
sessionId,
|
|
524
|
+
title: titleOf(events, sessionId),
|
|
525
|
+
total,
|
|
526
|
+
cost,
|
|
527
|
+
calls,
|
|
528
|
+
firstActive,
|
|
529
|
+
lastActive,
|
|
530
|
+
models,
|
|
531
|
+
turns: [...turnTotals.values()]
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
384
535
|
|
|
385
536
|
// src/wire.ts
|
|
386
537
|
function writeJson(res, status, body, headers) {
|
|
@@ -419,6 +570,15 @@ function apply(ctx) {
|
|
|
419
570
|
const payload = await (force ? usageMemo.refresh() : usageMemo.get());
|
|
420
571
|
const headers = payload.ok ? { "cache-control": `private, max-age=${Math.round(USAGE_TTL_MS / 1e3)}` } : { "cache-control": "no-store" };
|
|
421
572
|
writeJson(res, 200, payload, headers);
|
|
573
|
+
} else if (pathname === "/api/dsh-usage-dashboard/session") {
|
|
574
|
+
const id = url.searchParams.get("id");
|
|
575
|
+
if (id === null || id.trim() === "" || !isValidSessionId(id)) {
|
|
576
|
+
const error = id === null || id.trim() === "" ? "\u7F3A\u5C11\u4F1A\u8BDD id" : "\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5";
|
|
577
|
+
writeJson(res, 400, { ok: false, error }, { "cache-control": "no-store" });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const payload = await fetchSessionUsage(ctx.sessionPersistence, id);
|
|
581
|
+
writeJson(res, 200, payload, { "cache-control": "no-store" });
|
|
422
582
|
} else {
|
|
423
583
|
writeJson(res, 404, { ok: false, error: "not found" });
|
|
424
584
|
}
|