dsh-token-total 1.0.4 → 1.0.6
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 +16 -2
- package/lib/client.js +1163 -62
- package/lib/index.js +392 -1
- package/lib/types/client/ModelLogo.d.ts +18 -0
- package/lib/types/client/SessionUsageView.d.ts +14 -0
- package/lib/types/client/api.d.ts +3 -1
- package/lib/types/client/format.d.ts +1 -1
- package/lib/types/client/index.d.ts +2 -0
- package/lib/types/client/model-logos-core.d.ts +11 -0
- package/lib/types/client/model-logos-data.d.ts +8 -0
- package/lib/types/client/session-usage-locales.d.ts +62 -0
- package/lib/types/client/use-session-detail.d.ts +8 -0
- package/lib/types/routes.d.ts +2 -2
- package/lib/types/session-detail.d.ts +3 -0
- package/lib/types/wire.d.ts +55 -0
- package/package.json +1 -1
- package/src/client/ModelLogo.tsx +29 -0
- package/src/client/SessionUsageView.module.css +122 -0
- package/src/client/SessionUsageView.tsx +216 -0
- package/src/client/TokenTotalPanel.tsx +31 -19
- package/src/client/api.ts +8 -2
- package/src/client/format.ts +4 -2
- package/src/client/index.tsx +20 -1
- package/src/client/model-logos-core.ts +101 -0
- package/src/client/model-logos-data.ts +58 -0
- package/src/client/session-usage-locales.ts +50 -0
- package/src/client/use-session-detail.ts +50 -0
- package/src/collector.ts +7 -0
- package/src/index.ts +8 -1
- package/src/routes.ts +11 -2
- package/src/session-detail.ts +182 -0
- package/src/wire.ts +56 -0
package/lib/index.js
CHANGED
|
@@ -1400,6 +1400,8 @@ var UsageCollector = class UsageCollector {
|
|
|
1400
1400
|
const clean = !signal?.aborted && this.status.error === void 0;
|
|
1401
1401
|
if ((target === "hours" || dayCursorWasEmpty) && clean) await this.store.markHoursReady();
|
|
1402
1402
|
if ((target === "failures" || dayCursorWasEmpty) && clean) await this.store.markFailuresReady();
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
this.status.error = err instanceof Error ? err.message : String(err);
|
|
1403
1405
|
} finally {
|
|
1404
1406
|
this.status.running = false;
|
|
1405
1407
|
}
|
|
@@ -1495,6 +1497,383 @@ async function resolveModelNames(modelRefs, llm) {
|
|
|
1495
1497
|
return names;
|
|
1496
1498
|
}
|
|
1497
1499
|
//#endregion
|
|
1500
|
+
//#region src/session-detail.ts
|
|
1501
|
+
const zero = () => ({
|
|
1502
|
+
uncachedInputTokens: 0,
|
|
1503
|
+
outputTokens: 0,
|
|
1504
|
+
cacheReadTokens: 0,
|
|
1505
|
+
cacheWriteTokens: 0
|
|
1506
|
+
});
|
|
1507
|
+
function add$1(a, b) {
|
|
1508
|
+
a.uncachedInputTokens += b.uncachedInputTokens;
|
|
1509
|
+
a.outputTokens += b.outputTokens;
|
|
1510
|
+
a.cacheReadTokens += b.cacheReadTokens;
|
|
1511
|
+
a.cacheWriteTokens += b.cacheWriteTokens;
|
|
1512
|
+
}
|
|
1513
|
+
function usageOf(x) {
|
|
1514
|
+
const u = x?.usage ?? x;
|
|
1515
|
+
return {
|
|
1516
|
+
uncachedInputTokens: Number(u?.inputTokens) || 0,
|
|
1517
|
+
outputTokens: Number(u?.outputTokens) || 0,
|
|
1518
|
+
cacheReadTokens: Number(u?.cacheReadTokens) || 0,
|
|
1519
|
+
cacheWriteTokens: Number(u?.cacheWriteTokens) || 0
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
function positive(u) {
|
|
1523
|
+
return u.uncachedInputTokens + u.outputTokens + u.cacheReadTokens + u.cacheWriteTokens > 0;
|
|
1524
|
+
}
|
|
1525
|
+
function routeOf(x) {
|
|
1526
|
+
const source = x?.message?.source ?? x?.source ?? x?.request ?? x?.context ?? x;
|
|
1527
|
+
return {
|
|
1528
|
+
provider: typeof source?.provider === "string" ? source.provider : "",
|
|
1529
|
+
model: typeof source?.model === "string" ? source.model : ""
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
function streamUsage(data) {
|
|
1533
|
+
let firstTime;
|
|
1534
|
+
let usage;
|
|
1535
|
+
for (const r of Array.isArray(data?.stream) ? data.stream : []) {
|
|
1536
|
+
const t = typeof r?.time === "number" ? r.time : void 0;
|
|
1537
|
+
if (firstTime === void 0 && t !== void 0 && (r?.type === "chunk" || r?.chunk?.type === "text" || r?.chunk?.type === "content")) firstTime = t;
|
|
1538
|
+
if (r?.type === "chunk" && r?.chunk?.type === "usage") usage = usageOf(r.chunk);
|
|
1539
|
+
}
|
|
1540
|
+
return {
|
|
1541
|
+
usage,
|
|
1542
|
+
firstTime
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
function headerOf$1(item) {
|
|
1546
|
+
const x = item;
|
|
1547
|
+
if (x?.header?.id && typeof x.header.id === "string") return x.header;
|
|
1548
|
+
return typeof x?.id === "string" && x.id ? x : null;
|
|
1549
|
+
}
|
|
1550
|
+
async function read(p, id, live) {
|
|
1551
|
+
if (live?.snapshotEvents && typeof live.inheritedEventCount === "number") return {
|
|
1552
|
+
events: live.snapshotEvents(),
|
|
1553
|
+
cut: Math.max(0, live.inheritedEventCount)
|
|
1554
|
+
};
|
|
1555
|
+
if (p.open) {
|
|
1556
|
+
const h = await p.open(id, "read");
|
|
1557
|
+
const out = [];
|
|
1558
|
+
try {
|
|
1559
|
+
for (let off = 0;;) {
|
|
1560
|
+
const page = await h.read(off, 500);
|
|
1561
|
+
if (!page.events.length) break;
|
|
1562
|
+
out.push(...page.events);
|
|
1563
|
+
off += page.events.length;
|
|
1564
|
+
}
|
|
1565
|
+
} finally {
|
|
1566
|
+
await h.close().catch(() => {});
|
|
1567
|
+
}
|
|
1568
|
+
return {
|
|
1569
|
+
events: out,
|
|
1570
|
+
cut: Math.max(0, Number(h.inheritedEventCount) || 0)
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
if (p.inspect) {
|
|
1574
|
+
const x = await p.inspect(id);
|
|
1575
|
+
return {
|
|
1576
|
+
events: x.events,
|
|
1577
|
+
cut: Math.max(0, Number(x.inheritedEventCount) || 0)
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
throw new Error("no readable persistence seam (need open or inspect)");
|
|
1581
|
+
}
|
|
1582
|
+
function nameOf(h, id) {
|
|
1583
|
+
const x = h;
|
|
1584
|
+
if (typeof x?.name === "string" && x.name) return x.name;
|
|
1585
|
+
if (typeof x?.title === "string" && x.title) return x.title;
|
|
1586
|
+
if (typeof x?.cwd === "string" && x.cwd) return x.cwd.split(/[\\/]/).filter(Boolean).pop() || id;
|
|
1587
|
+
return id;
|
|
1588
|
+
}
|
|
1589
|
+
async function one(p, id, header, live) {
|
|
1590
|
+
const { events, cut } = await read(p, id, live);
|
|
1591
|
+
const fold = new UsageFold();
|
|
1592
|
+
const totals = zero();
|
|
1593
|
+
const records = [];
|
|
1594
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1595
|
+
let route = live?.requestContext?.() ?? {};
|
|
1596
|
+
let ordinal = 0;
|
|
1597
|
+
let currentTurn = 0;
|
|
1598
|
+
let effort = "";
|
|
1599
|
+
let pendingTopic = "";
|
|
1600
|
+
const turnTopics = /* @__PURE__ */ new Map();
|
|
1601
|
+
const own = events.filter((e) => !(cut > 0 && typeof e.seq === "number" && e.seq < cut));
|
|
1602
|
+
let label = "", title = "";
|
|
1603
|
+
for (const ev of own) {
|
|
1604
|
+
const d = ev.data ?? {};
|
|
1605
|
+
if (!label && ev.type === "subagent/descriptor" && typeof d.label === "string" && d.label.trim()) label = d.label.trim();
|
|
1606
|
+
if (!title && ev.type === "session/title" && typeof d.title === "string" && d.title.trim()) title = d.title.trim();
|
|
1607
|
+
if (label && title) break;
|
|
1608
|
+
}
|
|
1609
|
+
const name = label || title || nameOf(header, id);
|
|
1610
|
+
for (const ev of own) {
|
|
1611
|
+
const d = ev.data ?? {};
|
|
1612
|
+
const turn = Number.isFinite(d.turn) ? d.turn : 0;
|
|
1613
|
+
const step = Number.isFinite(d.step) ? d.step : 0;
|
|
1614
|
+
if (ev.type === "request/context") {
|
|
1615
|
+
route = {
|
|
1616
|
+
...route,
|
|
1617
|
+
...routeOf(d)
|
|
1618
|
+
};
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
const r = routeOf(d);
|
|
1622
|
+
if (r.provider || r.model) route = {
|
|
1623
|
+
...route,
|
|
1624
|
+
...r
|
|
1625
|
+
};
|
|
1626
|
+
if (ev.type === "turn/start") {
|
|
1627
|
+
currentTurn = turn;
|
|
1628
|
+
if (pendingTopic && !turnTopics.has(currentTurn)) {
|
|
1629
|
+
turnTopics.set(currentTurn, pendingTopic);
|
|
1630
|
+
pendingTopic = "";
|
|
1631
|
+
}
|
|
1632
|
+
} else if (ev.type === "user/message" && d.source?.kind === "user") {
|
|
1633
|
+
const block = (Array.isArray(d.content) ? d.content : []).find((b) => b?.type === "text" && typeof b.text === "string");
|
|
1634
|
+
const text = String(block?.text ?? "").replace(/\s+/g, " ").trim();
|
|
1635
|
+
if (text) {
|
|
1636
|
+
if (currentTurn > 0 && !turnTopics.has(currentTurn)) turnTopics.set(currentTurn, text);
|
|
1637
|
+
else pendingTopic = text;
|
|
1638
|
+
}
|
|
1639
|
+
} else if (ev.type === "request/header") {
|
|
1640
|
+
const e = d.header?.config?.reasoningEffort;
|
|
1641
|
+
if (typeof e === "string" && e) {
|
|
1642
|
+
effort = e;
|
|
1643
|
+
for (const x of records) if (x.status === "running") x.effort = e;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
let rec;
|
|
1647
|
+
if (ev.type === "step/start" || ev.type === "llm/retry-started") {
|
|
1648
|
+
const key = `${turn}:${step}`;
|
|
1649
|
+
if (ev.type === "llm/retry-started" || !byKey.has(key)) {
|
|
1650
|
+
const previous = byKey.get(key);
|
|
1651
|
+
if (previous && ev.type === "llm/retry-started" && previous.status === "running") {
|
|
1652
|
+
previous.status = "failed";
|
|
1653
|
+
previous.finishedAt = ev.time;
|
|
1654
|
+
}
|
|
1655
|
+
rec = {
|
|
1656
|
+
id: `${id}:${turn}:${step}:${ordinal++}`,
|
|
1657
|
+
sessionId: id,
|
|
1658
|
+
sessionName: name,
|
|
1659
|
+
turn,
|
|
1660
|
+
step,
|
|
1661
|
+
startedAt: ev.time,
|
|
1662
|
+
finishedAt: null,
|
|
1663
|
+
provider: r.provider || route.provider || "",
|
|
1664
|
+
model: r.model || route.model || "",
|
|
1665
|
+
status: "running",
|
|
1666
|
+
totals: zero(),
|
|
1667
|
+
firstTokenMs: null,
|
|
1668
|
+
tokensPerSecond: null,
|
|
1669
|
+
turnName: "",
|
|
1670
|
+
effort,
|
|
1671
|
+
_attempt: ev.type === "llm/retry-started" ? 1 : 0,
|
|
1672
|
+
_lastUsage: zero()
|
|
1673
|
+
};
|
|
1674
|
+
records.push(rec);
|
|
1675
|
+
byKey.set(key, rec);
|
|
1676
|
+
}
|
|
1677
|
+
} else if (ev.type === "assistant/chunk" || ev.type === "assistant/message" || ev.type === "assistant/attempt") {
|
|
1678
|
+
rec = byKey.get(`${turn}:${step}`);
|
|
1679
|
+
if (!rec) {
|
|
1680
|
+
rec = {
|
|
1681
|
+
id: `${id}:${turn}:${step}:${ordinal++}`,
|
|
1682
|
+
sessionId: id,
|
|
1683
|
+
sessionName: name,
|
|
1684
|
+
turn,
|
|
1685
|
+
step,
|
|
1686
|
+
startedAt: ev.time,
|
|
1687
|
+
finishedAt: null,
|
|
1688
|
+
provider: r.provider || route.provider || "",
|
|
1689
|
+
model: r.model || route.model || "",
|
|
1690
|
+
status: "running",
|
|
1691
|
+
totals: zero(),
|
|
1692
|
+
firstTokenMs: null,
|
|
1693
|
+
tokensPerSecond: null,
|
|
1694
|
+
turnName: "",
|
|
1695
|
+
effort,
|
|
1696
|
+
_attempt: 0,
|
|
1697
|
+
_lastUsage: zero()
|
|
1698
|
+
};
|
|
1699
|
+
records.push(rec);
|
|
1700
|
+
byKey.set(`${turn}:${step}`, rec);
|
|
1701
|
+
}
|
|
1702
|
+
if (r.provider) rec.provider = r.provider;
|
|
1703
|
+
if (r.model) rec.model = r.model;
|
|
1704
|
+
const su = streamUsage(d);
|
|
1705
|
+
const u = ev.type === "assistant/message" ? d.usage ? usageOf(d) : su.usage : ev.type === "assistant/chunk" ? d.chunk?.type === "usage" ? usageOf(d.chunk) : void 0 : d.usage ? usageOf(d) : su.usage;
|
|
1706
|
+
if (u) {
|
|
1707
|
+
rec._lastUsage = u;
|
|
1708
|
+
rec.totals = u;
|
|
1709
|
+
if (positive(u) && rec.firstTokenMs === null) {
|
|
1710
|
+
const rawFirst = su.firstTime ?? (typeof d.chunk?.time === "number" ? d.chunk.time : ev.time);
|
|
1711
|
+
rec.firstTokenMs = Math.max(0, rawFirst - rec.startedAt);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (ev.type !== "assistant/chunk") {
|
|
1715
|
+
rec.finishedAt = ev.time;
|
|
1716
|
+
rec.status = ev.type === "assistant/attempt" ? "failed" : "completed";
|
|
1717
|
+
}
|
|
1718
|
+
const elapsedMs = rec.finishedAt === null ? null : Math.max(0, rec.finishedAt - rec.startedAt);
|
|
1719
|
+
if (elapsedMs !== null && elapsedMs > 0 && rec.totals.outputTokens > 0) rec.tokensPerSecond = rec.totals.outputTokens / (elapsedMs / 1e3);
|
|
1720
|
+
} else if (ev.type === "turn/end") {
|
|
1721
|
+
const recs = records.filter((x) => x.turn === turn && x.status === "running");
|
|
1722
|
+
for (const x of recs) {
|
|
1723
|
+
x.status = "interrupted";
|
|
1724
|
+
x.finishedAt = ev.time;
|
|
1725
|
+
}
|
|
1726
|
+
} else if (ev.type === "compaction/summary") {
|
|
1727
|
+
const u = usageOf(d);
|
|
1728
|
+
rec = {
|
|
1729
|
+
id: `${id}:compaction:${ordinal++}`,
|
|
1730
|
+
sessionId: id,
|
|
1731
|
+
sessionName: name,
|
|
1732
|
+
turn: 0,
|
|
1733
|
+
step: 0,
|
|
1734
|
+
startedAt: ev.time,
|
|
1735
|
+
finishedAt: ev.time,
|
|
1736
|
+
provider: typeof d.provider === "string" ? d.provider : "",
|
|
1737
|
+
model: typeof d.model === "string" ? d.model : "",
|
|
1738
|
+
status: "completed",
|
|
1739
|
+
totals: u,
|
|
1740
|
+
firstTokenMs: null,
|
|
1741
|
+
tokensPerSecond: null,
|
|
1742
|
+
turnName: "",
|
|
1743
|
+
effort: "",
|
|
1744
|
+
_attempt: 0,
|
|
1745
|
+
_lastUsage: u
|
|
1746
|
+
};
|
|
1747
|
+
records.push(rec);
|
|
1748
|
+
}
|
|
1749
|
+
const sample = fold.fold(ev);
|
|
1750
|
+
if (sample && !sample.turn) add$1(totals, {
|
|
1751
|
+
uncachedInputTokens: sample.inputTokens,
|
|
1752
|
+
outputTokens: sample.outputTokens,
|
|
1753
|
+
cacheReadTokens: sample.cacheReadTokens,
|
|
1754
|
+
cacheWriteTokens: sample.cacheWriteTokens
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
const requests = records.length;
|
|
1758
|
+
const firsts = records.map((x) => x.firstTokenMs).filter((x) => x !== null);
|
|
1759
|
+
const outputSeconds = records.reduce((n, x) => n + x.totals.outputTokens, 0);
|
|
1760
|
+
const elapsed = records.reduce((n, x) => n + (x.firstTokenMs ?? 0), 0);
|
|
1761
|
+
for (const x of records) {
|
|
1762
|
+
x.turnName = turnTopics.get(x.turn) ?? "";
|
|
1763
|
+
delete x._attempt;
|
|
1764
|
+
delete x._lastUsage;
|
|
1765
|
+
}
|
|
1766
|
+
return {
|
|
1767
|
+
id,
|
|
1768
|
+
name,
|
|
1769
|
+
header,
|
|
1770
|
+
totals,
|
|
1771
|
+
records,
|
|
1772
|
+
requests,
|
|
1773
|
+
averageFirstTokenMs: firsts.length ? firsts.reduce((a, b) => a + b, 0) / firsts.length : null,
|
|
1774
|
+
tokensPerSecond: elapsed > 0 ? outputSeconds / (elapsed / 1e3) : null
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
async function buildSessionUsageDetail(persistence, sessions, rootId, ctx) {
|
|
1778
|
+
const headers = (await persistence.list(typeof persistence.open === "function" ? void 0 : void 0)).map(headerOf$1).filter((x) => x !== null);
|
|
1779
|
+
const live = new Map((sessions?.list() ?? []).map((x) => [x.id, x]));
|
|
1780
|
+
const children = /* @__PURE__ */ new Map();
|
|
1781
|
+
for (const h of headers) if (h.parentSession) children.set(h.parentSession, [...children.get(h.parentSession) ?? [], h.id]);
|
|
1782
|
+
for (const h of live.values()) {
|
|
1783
|
+
const p = h.header?.parentSession ?? h.parentSession;
|
|
1784
|
+
if (p) children.set(p, [...(children.get(p) ?? []).filter((x) => x !== h.id), h.id]);
|
|
1785
|
+
}
|
|
1786
|
+
const ids = [];
|
|
1787
|
+
const seen = /* @__PURE__ */ new Set([rootId]);
|
|
1788
|
+
const q = [rootId];
|
|
1789
|
+
while (q.length) for (const c of children.get(q.shift()) ?? []) if (!seen.has(c)) {
|
|
1790
|
+
seen.add(c);
|
|
1791
|
+
ids.push(c);
|
|
1792
|
+
q.push(c);
|
|
1793
|
+
}
|
|
1794
|
+
const all = [rootId, ...ids];
|
|
1795
|
+
const parts = await Promise.all(all.map((id) => one(persistence, id, headers.find((h) => h.id === id), live.get(id))));
|
|
1796
|
+
const root = parts[0];
|
|
1797
|
+
const totals = zero();
|
|
1798
|
+
const models = /* @__PURE__ */ new Map();
|
|
1799
|
+
const records = parts.flatMap((x) => x.records);
|
|
1800
|
+
for (const p of parts) add$1(totals, p.totals);
|
|
1801
|
+
for (const r of records) {
|
|
1802
|
+
const key = r.model || "(unknown)";
|
|
1803
|
+
const m = models.get(key) ?? {
|
|
1804
|
+
totals: zero(),
|
|
1805
|
+
requests: 0,
|
|
1806
|
+
providers: /* @__PURE__ */ new Set()
|
|
1807
|
+
};
|
|
1808
|
+
add$1(m.totals, r.totals);
|
|
1809
|
+
m.requests++;
|
|
1810
|
+
if (r.provider) m.providers.add(r.provider);
|
|
1811
|
+
models.set(key, m);
|
|
1812
|
+
}
|
|
1813
|
+
const subagents = parts.slice(1).map((p) => ({
|
|
1814
|
+
sessionId: p.id,
|
|
1815
|
+
name: p.name,
|
|
1816
|
+
totals: p.totals,
|
|
1817
|
+
requests: p.requests,
|
|
1818
|
+
averageFirstTokenMs: p.averageFirstTokenMs,
|
|
1819
|
+
tokensPerSecond: p.tokensPerSecond
|
|
1820
|
+
}));
|
|
1821
|
+
const routeMap = /* @__PURE__ */ new Map();
|
|
1822
|
+
for (const r of records) if (r.provider || r.model) {
|
|
1823
|
+
const id = `${r.provider}/${r.model}`;
|
|
1824
|
+
routeMap.set(id, {
|
|
1825
|
+
id,
|
|
1826
|
+
provider: r.provider,
|
|
1827
|
+
name: r.provider,
|
|
1828
|
+
model: r.model,
|
|
1829
|
+
status: r.status === "running" ? "running" : r.status === "failed" ? "failed" : "completed"
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
const llm = ctx?.get?.("llm");
|
|
1833
|
+
for (const x of llm?.listProviders?.() ?? []) if (x?.id && !routeMap.has(x.id)) routeMap.set(x.id, {
|
|
1834
|
+
id: x.id,
|
|
1835
|
+
provider: x.id,
|
|
1836
|
+
name: x.name || x.id,
|
|
1837
|
+
model: "",
|
|
1838
|
+
status: "idle"
|
|
1839
|
+
});
|
|
1840
|
+
const providerNames = resolveProviderNames(llm);
|
|
1841
|
+
const refOf = (provider, model) => provider ? `${provider}/${model}` : model;
|
|
1842
|
+
const refs = [];
|
|
1843
|
+
for (const [model, m] of models) if (model !== "(unknown)") {
|
|
1844
|
+
if (m.providers.size) for (const provider of m.providers) refs.push(refOf(provider, model));
|
|
1845
|
+
else refs.push(refOf("", model));
|
|
1846
|
+
}
|
|
1847
|
+
const names = await resolveModelNames(refs, llm);
|
|
1848
|
+
const displayNameOf = (model, providers) => {
|
|
1849
|
+
const keys = providers.size ? [...providers].map((provider) => refOf(provider, model)) : [refOf("", model)];
|
|
1850
|
+
for (const key of keys) {
|
|
1851
|
+
const resolved = names.get(key)?.trim();
|
|
1852
|
+
if (resolved) return resolved;
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
return {
|
|
1856
|
+
sessionId: rootId,
|
|
1857
|
+
totals,
|
|
1858
|
+
requests: parts.reduce((n, p) => n + p.requests, 0),
|
|
1859
|
+
averageFirstTokenMs: root.averageFirstTokenMs,
|
|
1860
|
+
tokensPerSecond: root.tokensPerSecond,
|
|
1861
|
+
subagents,
|
|
1862
|
+
models: [...models.entries()].map(([model, x]) => {
|
|
1863
|
+
const name = displayNameOf(model, x.providers);
|
|
1864
|
+
return {
|
|
1865
|
+
model,
|
|
1866
|
+
...name ? { name } : {},
|
|
1867
|
+
providers: [...x.providers].map((provider) => providerNames[provider] || provider),
|
|
1868
|
+
totals: x.totals,
|
|
1869
|
+
requests: x.requests
|
|
1870
|
+
};
|
|
1871
|
+
}),
|
|
1872
|
+
records,
|
|
1873
|
+
routes: [...routeMap.values()]
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
//#endregion
|
|
1498
1877
|
//#region src/wire.ts
|
|
1499
1878
|
/** Default cap for a JSON request body. The panel posts tiny objects
|
|
1500
1879
|
* ({ range, from?, to? }); the cap only bounds a misbehaving trusted
|
|
@@ -1832,6 +2211,16 @@ function buildUsageRoute(ctx, deps) {
|
|
|
1832
2211
|
});
|
|
1833
2212
|
return;
|
|
1834
2213
|
}
|
|
2214
|
+
if (method === "POST" && path === "/token-total/api/session-detail") {
|
|
2215
|
+
const body = await readJsonBody(req);
|
|
2216
|
+
const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
|
|
2217
|
+
if (sessionId === "" || sessionId.length > 200) throw new UsageError(400, "token total: session needs a valid sessionId");
|
|
2218
|
+
writeJson(res, {
|
|
2219
|
+
ok: true,
|
|
2220
|
+
value: await buildSessionUsageDetail(deps.persistence(), deps.sessions(), sessionId, ctx)
|
|
2221
|
+
});
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
1835
2224
|
if (method === "POST" && path === "/token-total/api/status") {
|
|
1836
2225
|
writeJson(res, {
|
|
1837
2226
|
ok: true,
|
|
@@ -2191,7 +2580,9 @@ function apply(ctx, config = {}) {
|
|
|
2191
2580
|
await rescan();
|
|
2192
2581
|
await ensureHourHistory();
|
|
2193
2582
|
await ensureFailureHistory();
|
|
2194
|
-
})()
|
|
2583
|
+
})().catch((err) => {
|
|
2584
|
+
ctx.logger?.warn(`[dsh-token-total] boot scan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2585
|
+
});
|
|
2195
2586
|
return () => {
|
|
2196
2587
|
scanController?.abort();
|
|
2197
2588
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ModelLogoProps {
|
|
2
|
+
/** Raw model id; may itself be a "provider/model" ref. */
|
|
3
|
+
model: string;
|
|
4
|
+
/** Resolved model display name (the by-model row's label) — matched with
|
|
5
|
+
* the raw id, so an opaque route id beside a brandful name still brands. */
|
|
6
|
+
name?: string;
|
|
7
|
+
/** Provider route id — a secondary match signal for opaque model ids. */
|
|
8
|
+
provider?: string;
|
|
9
|
+
/** Resolved provider display names (the by-model row's split), matched last. */
|
|
10
|
+
providers?: ReadonlyArray<string>;
|
|
11
|
+
/** Rendered square size; the record rows read at 13px. */
|
|
12
|
+
size?: number;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
/** One model's brand logo inline. The markup is a bundled constant (never
|
|
16
|
+
* request data) rendered as-is so each brand keeps its own gradients,
|
|
17
|
+
* circles and multi-path fills; ids inside stay unique across brands. */
|
|
18
|
+
export declare function ModelLogo({ model, name, provider, providers, size, className }: ModelLogoProps): JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
import type { SessionUsageDetail } from '../wire.ts';
|
|
3
|
+
import { SESSION_USAGE_NS, type SessionUsageKey } from './session-usage-locales.ts';
|
|
4
|
+
type T = (key: SessionUsageKey) => string;
|
|
5
|
+
export type SessionUsageViewProps = PropsLocale<typeof SESSION_USAGE_NS> & {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
};
|
|
8
|
+
/** Pure presentation export also used by offline visual regression fixtures. */
|
|
9
|
+
export declare function SessionUsageContent({ detail, t }: {
|
|
10
|
+
detail: SessionUsageDetail;
|
|
11
|
+
t: T;
|
|
12
|
+
}): JSX.Element;
|
|
13
|
+
export declare function SessionUsageView({ sessionId, t }: SessionUsageViewProps): JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* prefix-stripping proxy; the host's own route key stays absolute
|
|
12
12
|
* (`/token-total/api` in src/routes.ts), because only the browser half is relative.
|
|
13
13
|
*/
|
|
14
|
-
import type { BackfillStatus, SessionUsageTree, TokenTotalRange, TokenTotalRequest } from '../wire.ts';
|
|
14
|
+
import type { BackfillStatus, SessionUsageDetail, SessionUsageTree, TokenTotalRange, TokenTotalRequest } from '../wire.ts';
|
|
15
15
|
/** One wire failure. */
|
|
16
16
|
export declare class UsageApiError extends Error {
|
|
17
17
|
readonly code: string;
|
|
@@ -29,3 +29,5 @@ export declare function rebuildStats(): Promise<BackfillStatus>;
|
|
|
29
29
|
* covers only the main agent, so this is what makes a delegating session's
|
|
30
30
|
* cost visible. */
|
|
31
31
|
export declare function fetchSessionUsage(sessionId: string): Promise<SessionUsageTree>;
|
|
32
|
+
/** Log-owned requests and live route states for the per-session Usage view. */
|
|
33
|
+
export declare function fetchSessionDetail(sessionId: string, signal?: AbortSignal): Promise<SessionUsageDetail>;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* from the shared zero-dependency module so the host and client halves share
|
|
8
8
|
* one implementation.
|
|
9
9
|
*/
|
|
10
|
-
export { providerOf, modelNameOf, daysInRange as daysBetween } from '../shared.ts';
|
|
10
|
+
export { providerOf, modelNameOf, normalizeModelName, daysInRange as daysBetween } from '../shared.ts';
|
|
11
11
|
/** Exact token counts with thousands separators — no 万/亿/k/M units, the
|
|
12
12
|
* user reads the precise number (long values shrink to fit via FitText).
|
|
13
13
|
* Compact units stay on the axis labels and card sub-values only. */
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import type { Context, UsagePluginConfigOwnerProps } from '../context-types.ts';
|
|
22
22
|
import type { PropsLocale, PropsRuntime, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
|
|
23
23
|
import { LOCALE_NS, type TokenTotalKey } from './locales.ts';
|
|
24
|
+
import { type SessionUsageKey } from './session-usage-locales.ts';
|
|
24
25
|
/** This bundle's npm package name — the key the Plugins page matches on. */
|
|
25
26
|
export declare const BUNDLE_NAME = "dsh-token-total";
|
|
26
27
|
/** The id shared by the sidebar row and the main panel it selects. */
|
|
@@ -36,6 +37,7 @@ export type TokenTotalPanelPageProps = PropsLocale<typeof LOCALE_NS> & {
|
|
|
36
37
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
37
38
|
interface LocaleNamespaceMap {
|
|
38
39
|
'tokenTotal': TokenTotalKey;
|
|
40
|
+
'sessionUsage': SessionUsageKey;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
export declare const inject: string[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Brand ids in match order — every id here must ship markup. */
|
|
2
|
+
export declare const MODEL_LOGO_BRANDS: ReadonlyArray<string>;
|
|
3
|
+
/** Resolve the brand id for one usage record. The model's own naming (raw
|
|
4
|
+
* model id plus its resolved display name) decides the brand when it can;
|
|
5
|
+
* the provider fields (route id and provider display names) are the fallback
|
|
6
|
+
* for opaque model ids, so a Llama served through a vertex/google route keeps
|
|
7
|
+
* the Llama mark. Empty/undefined parts are ignored; null when no bundled
|
|
8
|
+
* brand matches. */
|
|
9
|
+
export declare function resolveModelLogo(provider: string | null | undefined, model: string | null | undefined, name?: string | null, providers?: ReadonlyArray<string>): string | null;
|
|
10
|
+
/** Inner SVG markup of a brand's logo, or undefined for an unknown id. */
|
|
11
|
+
export declare function modelLogoMarkup(id: string | null): string | undefined;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official per-brand logos for the usage panel: the inner markup of each
|
|
3
|
+
* brand's 24x24 SVG, extracted verbatim from @lobehub/icons-static-svg
|
|
4
|
+
* (MIT). Regenerate from .tmp/gen-logos.mjs after refreshing .tmp/logos/;
|
|
5
|
+
* brand colors are baked in, so no fill follows currentColor. Brand
|
|
6
|
+
* matching lives in model-logos-core.ts.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MODEL_LOGOS: Readonly<Record<string, string>>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Session Usage view translations, independent of the global dashboard. */
|
|
2
|
+
export declare const SESSION_USAGE_NS = "sessionUsage";
|
|
3
|
+
export declare const usageZh: {
|
|
4
|
+
tab: string;
|
|
5
|
+
requests: string;
|
|
6
|
+
tokens: string;
|
|
7
|
+
hit: string;
|
|
8
|
+
first: string;
|
|
9
|
+
speed: string;
|
|
10
|
+
input: string;
|
|
11
|
+
output: string;
|
|
12
|
+
read: string;
|
|
13
|
+
write: string;
|
|
14
|
+
requestsCount: string;
|
|
15
|
+
inputFull: string;
|
|
16
|
+
outputFull: string;
|
|
17
|
+
readFull: string;
|
|
18
|
+
writeFull: string;
|
|
19
|
+
cacheHit: string;
|
|
20
|
+
compaction: string;
|
|
21
|
+
reqTimes: string;
|
|
22
|
+
children: string;
|
|
23
|
+
session: string;
|
|
24
|
+
models: string;
|
|
25
|
+
model: string;
|
|
26
|
+
records: string;
|
|
27
|
+
expand: string;
|
|
28
|
+
collapse: string;
|
|
29
|
+
loading: string;
|
|
30
|
+
empty: string;
|
|
31
|
+
noChildren: string;
|
|
32
|
+
noModels: string;
|
|
33
|
+
scope: string;
|
|
34
|
+
note: string;
|
|
35
|
+
failedLoad: string;
|
|
36
|
+
retry: string;
|
|
37
|
+
completed: string;
|
|
38
|
+
running: string;
|
|
39
|
+
failed: string;
|
|
40
|
+
interrupted: string;
|
|
41
|
+
main: string;
|
|
42
|
+
turn: string;
|
|
43
|
+
step: string;
|
|
44
|
+
started: string;
|
|
45
|
+
duration: string;
|
|
46
|
+
firstDetail: string;
|
|
47
|
+
provider: string;
|
|
48
|
+
status: string;
|
|
49
|
+
exact: string;
|
|
50
|
+
answering: string;
|
|
51
|
+
firstWait: string;
|
|
52
|
+
sec: string;
|
|
53
|
+
tokenWord: string;
|
|
54
|
+
perSec: string;
|
|
55
|
+
cacheShort: string;
|
|
56
|
+
avgCache: string;
|
|
57
|
+
avgRate: string;
|
|
58
|
+
inProgress: string;
|
|
59
|
+
};
|
|
60
|
+
export type SessionUsageKey = keyof typeof usageZh;
|
|
61
|
+
export declare const usageEn: Record<SessionUsageKey, string>;
|
|
62
|
+
export declare const usageZhTW: Record<SessionUsageKey, string>;
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { UsageWebRoute } from './context-types.ts';
|
|
|
15
15
|
import type { UsageStore } from './store.ts';
|
|
16
16
|
import type { UsageCollector } from './collector.ts';
|
|
17
17
|
import type { Granularity } from './query.ts';
|
|
18
|
-
import type { BackfillStatus, SessionUsageRequest, TokenTotalRange, TokenTotalRequest } from './wire.ts';
|
|
18
|
+
import type { BackfillStatus, SessionUsageDetail, SessionUsageRequest, TokenTotalRange, TokenTotalRequest } from './wire.ts';
|
|
19
19
|
import type { SessionUsageService } from './session-usage.ts';
|
|
20
20
|
import { type TrustInputs } from './trust-fence.ts';
|
|
21
21
|
export interface RoutesDeps {
|
|
@@ -55,4 +55,4 @@ export declare function aggregateRange(store: UsageStore, from: string, to: stri
|
|
|
55
55
|
* The host mounts it via `ctx.webServer.register` (which takes ONE WebRoute,
|
|
56
56
|
* not an array 鈥?matching the real dsh-host-webserver contract). */
|
|
57
57
|
export declare function buildUsageRoute(ctx: Context, deps: RoutesDeps): UsageWebRoute;
|
|
58
|
-
export type { BackfillStatus, TokenTotalRange, TokenTotalRequest, SessionUsageRequest };
|
|
58
|
+
export type { BackfillStatus, TokenTotalRange, TokenTotalRequest, SessionUsageRequest, SessionUsageDetail };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Context, UsageSessionPersistence, UsageSessionStore } from './context-types.ts';
|
|
2
|
+
import type { SessionUsageDetail } from './wire.ts';
|
|
3
|
+
export declare function buildSessionUsageDetail(persistence: UsageSessionPersistence, sessions: UsageSessionStore | undefined, rootId: string, ctx?: Context): Promise<SessionUsageDetail>;
|
package/lib/types/wire.d.ts
CHANGED
|
@@ -129,6 +129,61 @@ export interface SessionUsageTree {
|
|
|
129
129
|
export interface SessionUsageRequest {
|
|
130
130
|
sessionId: string;
|
|
131
131
|
}
|
|
132
|
+
/** Detailed per-session usage view used by the Usage tab. */
|
|
133
|
+
export interface SessionUsageDetail {
|
|
134
|
+
sessionId: string;
|
|
135
|
+
totals: SessionUsageTotals;
|
|
136
|
+
requests: number;
|
|
137
|
+
averageFirstTokenMs: number | null;
|
|
138
|
+
tokensPerSecond: number | null;
|
|
139
|
+
subagents: Array<{
|
|
140
|
+
sessionId: string;
|
|
141
|
+
name: string;
|
|
142
|
+
totals: SessionUsageTotals;
|
|
143
|
+
requests: number;
|
|
144
|
+
averageFirstTokenMs: number | null;
|
|
145
|
+
tokensPerSecond: number | null;
|
|
146
|
+
}>;
|
|
147
|
+
models: Array<{
|
|
148
|
+
/** Raw model id from the historical route. Stays the row key and the
|
|
149
|
+
* fallback label (also the tooltip) when no display name is resolved. */
|
|
150
|
+
model: string;
|
|
151
|
+
/** The model selector's display name for this model, resolved host-side
|
|
152
|
+
* from the llm service metadata. Optional so an older host (or an
|
|
153
|
+
* unresolvable route) still serves a valid payload. */
|
|
154
|
+
name?: string;
|
|
155
|
+
/** Display names (raw ids when unknown) of every provider that served this
|
|
156
|
+
* model in the session, first-seen order. The by-model card shows these in
|
|
157
|
+
* gray when two rows end up sharing one display name. */
|
|
158
|
+
providers?: string[];
|
|
159
|
+
totals: SessionUsageTotals;
|
|
160
|
+
requests: number;
|
|
161
|
+
}>;
|
|
162
|
+
records: Array<{
|
|
163
|
+
id: string;
|
|
164
|
+
sessionId: string;
|
|
165
|
+
sessionName: string;
|
|
166
|
+
turn: number;
|
|
167
|
+
step: number;
|
|
168
|
+
startedAt: number;
|
|
169
|
+
finishedAt: number | null;
|
|
170
|
+
provider: string;
|
|
171
|
+
model: string;
|
|
172
|
+
status: 'running' | 'completed' | 'failed' | 'interrupted';
|
|
173
|
+
totals: SessionUsageTotals;
|
|
174
|
+
firstTokenMs: number | null;
|
|
175
|
+
tokensPerSecond: number | null;
|
|
176
|
+
turnName: string;
|
|
177
|
+
effort: string;
|
|
178
|
+
}>;
|
|
179
|
+
routes: Array<{
|
|
180
|
+
id: string;
|
|
181
|
+
provider: string;
|
|
182
|
+
name: string;
|
|
183
|
+
model: string;
|
|
184
|
+
status: 'idle' | 'running' | 'completed' | 'failed';
|
|
185
|
+
}>;
|
|
186
|
+
}
|
|
132
187
|
/** The backfill (historical session scan) progress state. */
|
|
133
188
|
export interface BackfillStatus {
|
|
134
189
|
running: boolean;
|
package/package.json
CHANGED