@cassius0924/dsh-usage-dashboard 0.4.0 → 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/lib/index.js CHANGED
@@ -235,7 +235,11 @@ function addUsageEvent(events, onEvent) {
235
235
  continue;
236
236
  }
237
237
  const [input = 0, output = 0, cache = 0, reasoning = 0] = values;
238
- onEvent(ev.time, input, output, cache, reasoning, provider, model);
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);
239
243
  result.usageRecords += 1;
240
244
  }
241
245
  return result;
@@ -477,7 +481,8 @@ async function fetchSessionUsage(persistence, sessionId) {
477
481
  let calls = 0;
478
482
  let firstActive = null;
479
483
  let lastActive = null;
480
- addUsageEvent(events, (time, input, output, cache, _reasoning, provider, model) => {
484
+ const turnTotals = /* @__PURE__ */ new Map();
485
+ addUsageEvent(events, (time, input, output, cache, _reasoning, provider, model, messageId, turn) => {
481
486
  const modelKey = `${provider}/${model}`;
482
487
  const eventCost = costOf(time, model, input, cache, output);
483
488
  const eventTotal = input + output + cache;
@@ -487,6 +492,15 @@ async function fetchSessionUsage(persistence, sessionId) {
487
492
  calls += 1;
488
493
  if (firstActive === null || time < firstActive) firstActive = time;
489
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
+ }
490
504
  });
491
505
  const models = [...modelTotals.entries()].map(([modelKey, bucket]) => {
492
506
  const slash = modelKey.indexOf("/");
@@ -513,7 +527,8 @@ async function fetchSessionUsage(persistence, sessionId) {
513
527
  calls,
514
528
  firstActive,
515
529
  lastActive,
516
- models
530
+ models,
531
+ turns: [...turnTotals.values()]
517
532
  }
518
533
  };
519
534
  }
package/lib/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/contract.ts", "../src/memo.ts", "../src/trust-fence.ts", "../src/pricing.ts", "../src/usage.ts", "../src/wire.ts", "../src/index.ts"],
4
- "sourcesContent": ["/** Wire contracts shared by the host API and the client fetchers. */\n\nexport interface BalanceInfo {\n currency: string\n total: string\n granted: string\n toppedUp: string\n}\n\nexport interface BalanceData {\n isAvailable: boolean\n balances: BalanceInfo[]\n}\n\nexport interface BalanceResponse {\n ok: boolean\n error?: string\n data?: BalanceData\n}\n\nexport interface DailyUsage {\n date: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\nexport interface HourlyUsage {\n hour: number\n total: number\n cost: number\n calls: number\n}\n\nexport interface HeatmapUsage {\n date: string\n total: number\n cost: number\n calls: number\n}\n\nexport interface UsageTotals {\n input: number\n output: number\n cache: number\n reasoning: number\n total: number\n cost: number\n calls: number\n /**\n * What the cache-hit tokens would have cost at cache-miss rates, minus what\n * they actually cost \u2014 i.e. the money prefix caching saved. Folded per\n * record so it respects each record's model and time-of-day rates.\n */\n cacheSavings: number\n}\n\n/** One per-day / per-hour point of a single model's series. */\nexport interface ModelSeriesPoint {\n total: number\n cost: number\n calls: number\n}\n\n/** Per-model usage aggregation: overall totals plus 30-day and 24-hour series. */\nexport interface ModelUsage {\n provider: string\n model: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n daily: ModelSeriesPoint[]\n hourly: ModelSeriesPoint[]\n}\n\n/** Usage rolled up over one time window. */\nexport interface PeriodUsage {\n total: number\n cost: number\n calls: number\n}\n\n/**\n * Time-anchored rollups. \"Accumulated since forever\" answers no question a\n * user actually has; these do: what did today cost, what has this month cost,\n * and is that more or less than the comparable window before it.\n */\nexport interface UsageSummary {\n today: PeriodUsage\n yesterday: PeriodUsage\n /** Current calendar month, to date. */\n month: PeriodUsage\n /** Previous calendar month up to the same day-of-month, so a mid-month\n * comparison is against a same-length window rather than a full month. */\n lastMonthToDate: PeriodUsage\n}\n\n/** CNY per 1M tokens for one model tier. */\nexport interface PricingRates {\n cacheHit: number\n input: number\n output: number\n}\n\nexport interface PricingTier {\n model: string\n peak: PricingRates\n /** Null while flat pricing is in effect (before the peak/off-peak switch). */\n offPeak: PricingRates | null\n}\n\n/** What the cost column was computed with, so the estimate can be audited. */\nexport interface PricingInfo {\n currency: string\n /** Date peak/off-peak pricing takes effect. */\n switchDate: string\n splitActive: boolean\n inPeakNow: boolean\n peakWindows: string[]\n tiers: PricingTier[]\n}\n\n/**\n * How usage divides between DeepSeek's peak and off-peak windows, plus the\n * same usage re-priced two ways \u2014 so the 2026-08-17 change can be answered\n * before it lands (\"what will this cost me\") and after (\"what would shifting\n * off-peak save me\").\n */\nexport interface PeakSplit {\n peak: PeriodUsage\n offPeak: PeriodUsage\n /** Every record priced under the peak/off-peak table, as actually classified. */\n peakEraCost: number\n /** Every record priced under that table as if it had landed off-peak. */\n offPeakEraCost: number\n}\n\n/** One session's share of the bill. */\nexport interface SessionCost {\n id: string\n /** The session's title from its log; falls back to a short id. */\n title: string\n total: number\n cost: number\n calls: number\n /** Epoch ms of the last usage record in this session. */\n lastActive: number\n}\n\n/**\n * What the local log replay could actually inspect. Totals can otherwise look\n * authoritative even when one corrupt session was skipped, so this metadata\n * travels with every usage response and lets the UI disclose partial results.\n */\nexport interface UsageCoverage {\n scope: 'local-dsh-session-logs'\n /** Session headers with a usable id returned by the local persistence. */\n listedSessions: number\n /** Session logs read successfully, including logs with no usage. */\n scannedSessions: number\n /** Valid assistant usage records folded into the totals. */\n usageRecords: number\n /** Usage records rejected because their time or token data was invalid. */\n skippedRecords: number\n /** Session logs that could not be read at all. */\n failedSessions: number\n /** Epoch ms of the first/last valid usage record across all local logs. */\n earliestAt: number | null\n latestAt: number | null\n}\n\nexport const USAGE_WINDOW_DAYS = [7, 30, 90, 365] as const\nexport type UsageWindowDays = (typeof USAGE_WINDOW_DAYS)[number]\n\n/**\n * Session ids observed in this codebase (host-issued, e.g.\n * `session-484a1c14-c6fe-4d6a-abfd-a2d8d2f664d5`) are always plain\n * ASCII-safe tokens \u2014 no documented format contract exists for them outside\n * this repo (`SessionPersistenceFace`/`readFrom` is implemented by the DSH\n * host, not here), so this is a defensive charset whitelist rather than an\n * exact-format check: letters, digits, `-`, `_` only, with a generous length\n * cap. It exists specifically to stop path-separator / `..` / null-byte\n * payloads from ever reaching `persistence.readFrom(id, 0)` \u2014 `id` on the\n * `/session` route arrives verbatim from the browser-controlled `?id=` query\n * parameter, unlike `fetchUsage()`'s session ids, which the host itself\n * enumerates via `persistence.list()`.\n */\nexport const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\nexport const isValidSessionId = (id: string): boolean => SESSION_ID_PATTERN.test(id)\n\n/** One internally consistent view of the same local logs over a calendar window. */\nexport interface UsageWindow {\n days: UsageWindowDays\n daily: DailyUsage[]\n hourly: HourlyUsage[]\n totals: UsageTotals\n models: ModelUsage[]\n /** Most expensive sessions inside this window, capped at SESSION_TOP_N. */\n sessions: SessionCost[]\n /** All sessions that contributed usage inside this window. */\n sessionCount: number\n}\n\nexport interface UsageData {\n daily: DailyUsage[]\n hourly: HourlyUsage[]\n heatmap: HeatmapUsage[]\n totals: UsageTotals\n models: ModelUsage[]\n summary: UsageSummary\n pricing: PricingInfo\n peakSplit: PeakSplit\n /** Most expensive sessions first, capped \u2014 see SESSION_TOP_N. */\n sessions: SessionCost[]\n /** How many sessions contributed usage in total. */\n sessionCount: number\n coverage: UsageCoverage\n windows: UsageWindow[]\n}\n\nexport interface UsageResponse {\n ok: boolean\n error?: string\n data?: UsageData\n}\n\n/**\n * One model's contribution to a single session's bill \u2014 the same field\n * shape as `ModelUsage` minus the `daily`/`hourly` series, which only make\n * sense aggregated across many sessions.\n */\nexport interface SessionModelUsage {\n provider: string\n model: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\n/**\n * Usage for exactly one session \u2014 what the \u300C\u989D\u5EA6\u300Dtab's own conversation\n * is costing, as opposed to `UsageData`'s account-wide totals. Fetched on\n * demand (see `fetchSessionUsage`), not part of the 5-minute `usage` memo:\n * a single session read is cheap and must reflect the session's live state.\n */\nexport interface SessionUsageData {\n sessionId: string\n /** The session's own title from its log; falls back to a short id. */\n title: string\n total: number\n cost: number\n calls: number\n /** Epoch ms of the first/last valid usage record in this session, or null\n * when the session has produced no usage yet. */\n firstActive: number | null\n lastActive: number | null\n /** Most expensive model first. */\n models: SessionModelUsage[]\n}\n\nexport interface SessionUsageResponse {\n ok: boolean\n error?: string\n data?: SessionUsageData\n}\n", "/**\n * Tiny TTL memo for the /api routes.\n *\n * The usage endpoint replays every session log, so it must not run on every\n * tab click; the balance endpoint also calls the DeepSeek API. The memo:\n *\n * - `get()` serves a successful cached response while it is within `ttlMs`,\n * otherwise recomputes;\n * - `refresh()` recomputes unconditionally and reseeds the cache;\n * - concurrent recomputes (of either kind) are deduped into one upstream\n * call;\n * - `ok:false` responses are never cached, so a transient failure is retried\n * on the next request.\n *\n * No timers are held, so nothing needs disposal on plugin unload.\n */\n\ninterface Entry<T> {\n value: T\n setAt: number\n}\n\ninterface MemoState<T> {\n entry: Entry<T> | undefined\n inflight: Promise<T> | undefined\n}\n\nexport interface Memo<T> {\n get(): Promise<T>\n refresh(): Promise<T>\n}\n\nexport function memoize<T extends { ok: boolean }>(ttlMs: number, compute: () => Promise<T>): Memo<T> {\n const state: MemoState<T> = { entry: undefined, inflight: undefined }\n\n const run = (): Promise<T> => {\n if (state.inflight !== undefined) return state.inflight\n const task = compute().then(\n (value) => {\n state.inflight = undefined\n if (value.ok) state.entry = { value, setAt: Date.now() }\n return value\n },\n (err: unknown) => {\n state.inflight = undefined\n throw err\n },\n )\n state.inflight = task\n return task\n }\n\n return {\n get: () => {\n const hit = state.entry\n if (hit !== undefined && hit.value.ok && Date.now() - hit.setAt < ttlMs) {\n return Promise.resolve(hit.value)\n }\n return run()\n },\n refresh: () => run(),\n }\n}\n", "/**\n * Browser-trust fence for the plugin's own /api route \u2014 the same fences the\n * harness's /api gateway applies: the Host header must name a loopback\n * authority, a browser-marked cross-site request is refused, and an attached\n * Origin must be exactly this authority.\n */\nimport type { IncomingMessage } from 'node:http'\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]'\n}\n\nexport function isTrustedApiRequest(req: IncomingMessage): boolean {\n const host = req.headers.host\n if (host === undefined) return false\n let hostUrl: URL\n try {\n hostUrl = new URL(`http://${host}`)\n } catch {\n return false\n }\n if (!isLoopbackHostname(hostUrl.hostname)) return false\n if (req.headers['sec-fetch-site'] === 'cross-site') return false\n const origin = req.headers.origin\n if (origin === undefined) return true\n try {\n return new URL(origin).host === hostUrl.host\n } catch {\n return false\n }\n}\n", "/**\n * DeepSeek API price table and cost estimation.\n *\n * Two things the previous flat constant got wrong: every model was billed at\n * deepseek-v4-pro rates (flash is ~3x cheaper), and the 2026-08-17 switch to\n * peak/off-peak pricing was not modelled at all.\n *\n * Rates are CNY per 1M tokens, from DeepSeek's 2026-08-13 price announcement.\n * Peak window is Beijing time 09:00\u201312:00 and 14:00\u201318:00; off-peak is half of\n * peak. Usage recorded before the switch is still costed at the old flat rates,\n * so historical days keep the price that was actually charged.\n *\n * Editing this table is the one place to touch when DeepSeek changes prices.\n */\nimport type { PricingInfo, PricingRates } from './contract.ts'\n\n/** 2026-08-17 00:00 Beijing time (UTC+8), when peak/off-peak pricing starts. */\nexport const PEAK_PRICING_FROM_MS = Date.UTC(2026, 7, 16, 16, 0, 0)\n\n/** Peak hours in Beijing time, as [startHour, endHour) pairs. */\nconst PEAK_WINDOWS: Array<[number, number]> = [[9, 12], [14, 18]]\n\nconst BEIJING_OFFSET_MS = 8 * 3_600_000\n\ntype Tier = 'pro' | 'flash'\n\n/** Flat rates in effect until 2026-08-17. */\nconst LEGACY_RATES: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.025, input: 3, output: 6 },\n flash: { cacheHit: 0.02, input: 1, output: 2 },\n}\n\n/** Peak rates from 2026-08-17; off-peak is exactly half of each. */\nconst PEAK_RATES: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.3, input: 9, output: 27 },\n flash: { cacheHit: 0.1, input: 3, output: 9 },\n}\n\nconst halved = (rates: PricingRates): PricingRates => ({\n cacheHit: rates.cacheHit / 2,\n input: rates.input / 2,\n output: rates.output / 2,\n})\n\n/** Which price tier a model name falls into; anything unrecognised is billed\n * as pro, the conservative (more expensive) guess. */\nexport function tierOf(model: string): Tier {\n return model.toLowerCase().includes('flash') ? 'flash' : 'pro'\n}\n\n/** Whether a moment falls in a peak window, judged in Beijing time so the\n * estimate does not drift with the machine's timezone. */\nexport function isPeak(timeMs: number): boolean {\n const hour = Math.floor((((timeMs + BEIJING_OFFSET_MS) % 86_400_000) + 86_400_000) % 86_400_000 / 3_600_000)\n return PEAK_WINDOWS.some(([from, to]) => hour >= from && hour < to)\n}\n\n/** Rates applying to one model at one moment. */\nexport function ratesAt(timeMs: number, model: string): PricingRates {\n const tier = tierOf(model)\n if (timeMs < PEAK_PRICING_FROM_MS) return LEGACY_RATES[tier]\n const peak = PEAK_RATES[tier]\n return isPeak(timeMs) ? peak : halved(peak)\n}\n\n/**\n * What prefix caching saved on one record: the cache-hit tokens re-priced at\n * the cache-miss rate, minus what they actually cost.\n */\nexport function cacheSavingOf(timeMs: number, model: string, cache: number): number {\n const rates = ratesAt(timeMs, model)\n return (cache * (rates.input - rates.cacheHit)) / 1_000_000\n}\n\nconst applyRates = (rates: PricingRates, input: number, cache: number, output: number): number =>\n (input * rates.input + cache * rates.cacheHit + output * rates.output) / 1_000_000\n\n/** Estimated CNY cost of one usage record. */\nexport function costOf(timeMs: number, model: string, input: number, cache: number, output: number): number {\n return applyRates(ratesAt(timeMs, model), input, cache, output)\n}\n\n/**\n * The same record priced under the peak/off-peak table regardless of when it\n * happened \u2014 `forceOffPeak` prices it as if it had landed in an idle window.\n * Used to answer \"what will the new pricing cost me\" before the switch and\n * \"what would shifting off-peak save\" after it.\n */\nexport function costUnderPeakEra(timeMs: number, model: string, input: number, cache: number, output: number, forceOffPeak = false): number {\n const peak = PEAK_RATES[tierOf(model)]\n const rates = !forceOffPeak && isPeak(timeMs) ? peak : halved(peak)\n return applyRates(rates, input, cache, output)\n}\n\n/** The price table to show the user, describing whatever is in effect now. */\nexport function pricingInfo(nowMs: number): PricingInfo {\n const split = nowMs >= PEAK_PRICING_FROM_MS\n return {\n currency: 'CNY',\n switchDate: '2026-08-17',\n splitActive: split,\n inPeakNow: split && isPeak(nowMs),\n peakWindows: ['09:00\u201312:00', '14:00\u201318:00'],\n tiers: (['pro', 'flash'] as Tier[]).map(tier => ({\n model: tier === 'pro' ? 'deepseek-v4-pro' : 'deepseek-v4-flash',\n peak: split ? PEAK_RATES[tier] : LEGACY_RATES[tier],\n offPeak: split ? halved(PEAK_RATES[tier]) : null,\n })),\n }\n}\n", "/**\n * Balance and usage data sources.\n *\n * - Balance: DeepSeek API `GET /user/balance` with the key resolved through\n * the credentials service (`DEEPSEEK_API_KEY`).\n * - Usage: token usage folded from the DSH session logs (`sessionPersistence`),\n * one record per `assistant/message` event, bucketed by local day / hour.\n */\nimport { isValidSessionId, USAGE_WINDOW_DAYS } from './contract.ts'\nimport type { BalanceResponse, ModelSeriesPoint, ModelUsage, PeakSplit, PeriodUsage, SessionCost, SessionModelUsage, SessionUsageResponse, UsageCoverage, UsageData, UsageResponse, UsageSummary, UsageWindowDays } from './contract.ts'\nimport type { CredentialsFace, SessionEventFace, SessionPersistenceFace } from './context.ts'\nimport { cacheSavingOf, costOf, costUnderPeakEra, isPeak, pricingInfo } from './pricing.ts'\n\nconst pad2 = (n: number): string => (n < 10 ? `0${n}` : String(n))\n\nfunction errorMessage(err: unknown): string {\n return (err as { message?: string } | null)?.message ?? String(err)\n}\n\nexport async function fetchBalance(credentials: CredentialsFace | undefined): Promise<BalanceResponse> {\n if (credentials === undefined) return { ok: false, error: '\u51ED\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528' }\n let cred: { value: string } | undefined\n try {\n cred = await credentials.resolve('DEEPSEEK_API_KEY')\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6 API Key \u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n if (cred === undefined || cred.value === '') {\n return { ok: false, error: '\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09' }\n }\n let res: Response\n try {\n res = await fetch('https://api.deepseek.com/user/balance', {\n headers: { authorization: `Bearer ${cred.value}` },\n signal: AbortSignal.timeout(20000),\n })\n } catch (err) {\n return { ok: false, error: `\u8BF7\u6C42\u4F59\u989D\u63A5\u53E3\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n if (!res.ok) return { ok: false, error: `\u4F59\u989D\u63A5\u53E3\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09` }\n let parsed: unknown\n try {\n parsed = await res.json()\n } catch {\n return { ok: false, error: '\u89E3\u6790\u4F59\u989D\u54CD\u5E94\u5931\u8D25' }\n }\n const p = parsed as {\n is_available?: boolean\n balance_infos?: Array<{ currency?: string; total_balance?: string; granted_balance?: string; topped_up_balance?: string }>\n }\n return {\n ok: true,\n data: {\n isAvailable: p.is_available === true,\n balances: (p.balance_infos ?? []).map(b => ({\n currency: b.currency ?? '',\n total: b.total_balance ?? '0',\n granted: b.granted_balance ?? '0',\n toppedUp: b.topped_up_balance ?? '0',\n })),\n },\n }\n}\n\ninterface Bucket {\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\nconst emptyBucket = (): Bucket => ({ input: 0, output: 0, cache: 0, total: 0, cost: 0, calls: 0 })\n\n/** Cost is computed once per event (it depends on the model and on whether\n * the event landed in a peak window) and folded into every bucket that\n * needs it \u2014 shared by the account-wide replay and the single-session one. */\nfunction bump(map: Map<string, Bucket>, key: string, input: number, output: number, cache: number, cost: number): void {\n const bucket = map.get(key) ?? emptyBucket()\n bucket.input += input\n bucket.output += output\n bucket.cache += cache\n // reasoningTokens is a subset of outputTokens; never double-count it.\n bucket.total += input + output + cache\n bucket.cost += cost\n bucket.calls += 1\n map.set(key, bucket)\n}\n\ntype OverallBucket = Bucket & { reasoning: number; cacheSavings: number }\nconst emptyOverall = (): OverallBucket => ({ ...emptyBucket(), reasoning: 0, cacheSavings: 0 })\n\ninterface WindowAccumulator {\n days: UsageWindowDays\n startKey: string\n endKey: string\n dayMap: Map<string, Bucket>\n hourMap: Map<string, Bucket>\n overall: OverallBucket\n modelTotals: Map<string, Bucket>\n modelDays: Map<string, Map<string, Bucket>>\n modelHours: Map<string, Map<string, Bucket>>\n sessions: SessionCost[]\n}\n\n/** How many sessions the ranking keeps; the rest are summarised by count. */\nconst SESSION_TOP_N = 6\n\n/**\n * The session's own title, folded from its log: `session/title` events are\n * append-only and the last one wins. It rides along with the usage replay, so\n * naming a session costs no extra read.\n */\nfunction titleOf(events: SessionEventFace[] | undefined, id: string): string {\n let title = ''\n if (events !== undefined) {\n for (const ev of events) {\n if (ev?.type !== 'session/title') continue\n const next = ev.data?.title\n if (typeof next === 'string' && next !== '') title = next\n }\n }\n return title !== '' ? title : `\u4F1A\u8BDD ${id.slice(0, 8)}`\n}\n\nconst dayKeyOf = (d: Date): string => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n\n/**\n * Roll the per-day buckets up into the windows the dashboard's summary card\n * shows. Everything is keyed by local date, matching how the buckets were\n * filled, so \"today\" means the user's today.\n */\nfunction summarize(dayMap: Map<string, Bucket>, now: Date): UsageSummary {\n const sum = (matches: (key: string) => boolean): PeriodUsage => {\n const acc: PeriodUsage = { total: 0, cost: 0, calls: 0 }\n for (const [key, bucket] of dayMap) {\n if (!matches(key)) continue\n acc.total += bucket.total\n acc.cost += bucket.cost\n acc.calls += bucket.calls\n }\n return acc\n }\n const todayKey = dayKeyOf(now)\n const yesterdayKey = dayKeyOf(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1))\n const monthPrefix = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-`\n const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)\n const lastMonthPrefix = `${lastMonth.getFullYear()}-${pad2(lastMonth.getMonth() + 1)}-`\n const dayOfMonth = now.getDate()\n return {\n today: sum(key => key === todayKey),\n yesterday: sum(key => key === yesterdayKey),\n month: sum(key => key.startsWith(monthPrefix)),\n lastMonthToDate: sum(key => key.startsWith(lastMonthPrefix) && Number(key.slice(8)) <= dayOfMonth),\n }\n}\n\n/**\n * Fold usage out of one session's event log. The model for every\n * `assistant/message` usage record is the one from the latest preceding\n * `request/header` event (each request logs one before dispatch), so usage\n * can be attributed per model. Events without any preceding header fall back\n * to the `''`/`''` (unknown) bucket.\n */\nfunction addUsageEvent(\n events: SessionEventFace[] | undefined,\n onEvent: (time: number, input: number, output: number, cache: number, reasoning: number, provider: string, model: string) => void,\n): Pick<UsageCoverage, 'usageRecords' | 'skippedRecords'> {\n const result = { usageRecords: 0, skippedRecords: 0 }\n if (events === undefined) return result\n let provider = ''\n let model = ''\n for (const ev of events) {\n if (ev?.type === 'request/header') {\n const cfg = ev.data?.header?.config\n if (cfg?.provider !== undefined && cfg?.model !== undefined) {\n provider = cfg.provider\n model = cfg.model\n }\n continue\n }\n if (ev?.type !== 'assistant/message') continue\n const usage = ev.data?.usage\n if (usage === undefined) continue\n const values = [usage.inputTokens, usage.outputTokens, usage.cacheReadTokens, usage.reasoningTokens]\n .map(value => value === undefined ? 0 : Number(value))\n if (typeof ev.time !== 'number' || !Number.isFinite(ev.time) || values.some(value => !Number.isFinite(value) || value < 0)) {\n result.skippedRecords += 1\n continue\n }\n const [input = 0, output = 0, cache = 0, reasoning = 0] = values\n onEvent(ev.time, input, output, cache, reasoning, provider, model)\n result.usageRecords += 1\n }\n return result\n}\n\nexport async function fetchUsage(persistence: SessionPersistenceFace | undefined, nowMs = Date.now()): Promise<UsageResponse> {\n if (persistence === undefined) return { ok: false, error: '\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528' }\n let headers: Array<{ id?: string; sessionId?: string }>\n try {\n headers = await persistence.list()\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n const dayMap = new Map<string, Bucket>()\n const hourMap = new Map<string, Bucket>()\n const overall = emptyOverall()\n // Per model: key `${provider}/${model}` -> totals and day/hour maps.\n const modelTotals = new Map<string, Bucket>()\n const modelDays = new Map<string, Map<string, Bucket>>()\n const modelHours = new Map<string, Map<string, Bucket>>()\n const sessions: SessionCost[] = []\n const coverage: UsageCoverage = {\n scope: 'local-dsh-session-logs',\n listedSessions: 0,\n scannedSessions: 0,\n usageRecords: 0,\n skippedRecords: 0,\n failedSessions: 0,\n earliestAt: null,\n latestAt: null,\n }\n // Peak / off-peak split, plus the same usage re-priced under the new table.\n const peakSplit: PeakSplit = {\n peak: { total: 0, cost: 0, calls: 0 },\n offPeak: { total: 0, cost: 0, calls: 0 },\n peakEraCost: 0,\n offPeakEraCost: 0,\n }\n\n const now = new Date(nowMs)\n const dateBefore = (days: number): Date => new Date(now.getFullYear(), now.getMonth(), now.getDate() - days)\n const windowAggregates: WindowAccumulator[] = USAGE_WINDOW_DAYS.map(days => ({\n days,\n startKey: dayKeyOf(dateBefore(days - 1)),\n endKey: dayKeyOf(now),\n dayMap: new Map(),\n hourMap: new Map(),\n overall: emptyOverall(),\n modelTotals: new Map(),\n modelDays: new Map(),\n modelHours: new Map(),\n sessions: [],\n }))\n\n const bumpOverall = (\n bucket: OverallBucket,\n input: number,\n output: number,\n cache: number,\n reasoning: number,\n cost: number,\n cacheSavings: number,\n ): void => {\n bucket.input += input\n bucket.output += output\n bucket.cache += cache\n bucket.reasoning += reasoning\n bucket.total += input + output + cache\n bucket.cost += cost\n bucket.cacheSavings += cacheSavings\n bucket.calls += 1\n }\n\n const bumpModel = (\n totals: Map<string, Bucket>,\n days: Map<string, Map<string, Bucket>>,\n hours: Map<string, Map<string, Bucket>>,\n modelKey: string,\n dayKey: string,\n hourKey: string,\n input: number,\n output: number,\n cache: number,\n cost: number,\n ): void => {\n bump(totals, modelKey, input, output, cache, cost)\n let dayBuckets = days.get(modelKey)\n if (dayBuckets === undefined) {\n dayBuckets = new Map()\n days.set(modelKey, dayBuckets)\n }\n bump(dayBuckets, dayKey, input, output, cache, cost)\n let hourBuckets = hours.get(modelKey)\n if (hourBuckets === undefined) {\n hourBuckets = new Map()\n hours.set(modelKey, hourBuckets)\n }\n bump(hourBuckets, hourKey, input, output, cache, cost)\n }\n\n const bumpSession = (session: SessionCost, time: number, total: number, cost: number): void => {\n session.total += total\n session.cost += cost\n session.calls += 1\n if (time > session.lastActive) session.lastActive = time\n }\n\n const sessionIds = headers\n .map(header => header.id ?? header.sessionId)\n .filter((sid): sid is string => sid !== undefined && sid !== '')\n coverage.listedSessions = sessionIds.length\n\n for (const sid of sessionIds) {\n try {\n const { events } = await persistence.readFrom(sid, 0)\n coverage.scannedSessions += 1\n const session: SessionCost = { id: sid, title: '', total: 0, cost: 0, calls: 0, lastActive: 0 }\n const windowSessions = windowAggregates.map((): SessionCost => ({ id: sid, title: '', total: 0, cost: 0, calls: 0, lastActive: 0 }))\n const scanned = addUsageEvent(events, (time, input, output, cache, reasoning, provider, model) => {\n if (coverage.earliestAt === null || time < coverage.earliestAt) coverage.earliestAt = time\n if (coverage.latestAt === null || time > coverage.latestAt) coverage.latestAt = time\n const d = new Date(time)\n const dayKey = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n const hourKey = String(d.getHours())\n const modelKey = `${provider}/${model}`\n const cost = costOf(time, model, input, cache, output)\n const cacheSavings = cacheSavingOf(time, model, cache)\n const eventTotal = input + output + cache\n bump(dayMap, dayKey, input, output, cache, cost)\n bump(hourMap, hourKey, input, output, cache, cost)\n bumpModel(modelTotals, modelDays, modelHours, modelKey, dayKey, hourKey, input, output, cache, cost)\n bumpOverall(overall, input, output, cache, reasoning, cost, cacheSavings)\n bumpSession(session, time, eventTotal, cost)\n for (let i = 0; i < windowAggregates.length; i++) {\n const aggregate = windowAggregates[i]\n if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue\n bump(aggregate.dayMap, dayKey, input, output, cache, cost)\n bump(aggregate.hourMap, hourKey, input, output, cache, cost)\n bumpModel(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, modelKey, dayKey, hourKey, input, output, cache, cost)\n bumpOverall(aggregate.overall, input, output, cache, reasoning, cost, cacheSavings)\n bumpSession(windowSessions[i], time, eventTotal, cost)\n }\n const window = isPeak(time) ? peakSplit.peak : peakSplit.offPeak\n window.total += eventTotal\n window.cost += cost\n window.calls += 1\n peakSplit.peakEraCost += costUnderPeakEra(time, model, input, cache, output)\n peakSplit.offPeakEraCost += costUnderPeakEra(time, model, input, cache, output, true)\n })\n coverage.usageRecords += scanned.usageRecords\n coverage.skippedRecords += scanned.skippedRecords\n if (session.calls > 0) {\n const title = titleOf(events, sid)\n session.title = title\n sessions.push(session)\n for (let i = 0; i < windowAggregates.length; i++) {\n const windowSession = windowSessions[i]\n if (windowSession.calls === 0) continue\n windowSession.title = title\n windowAggregates[i].sessions.push(windowSession)\n }\n }\n } catch {\n // One broken session log must not sink the whole dashboard.\n coverage.failedSessions += 1\n }\n }\n\n const makeDaily = (map: Map<string, Bucket>, count: number): UsageData['daily'] => {\n const result: UsageData['daily'] = []\n for (let i = count - 1; i >= 0; i--) {\n const key = dayKeyOf(dateBefore(i))\n const b = map.get(key) ?? emptyBucket()\n result.push({ date: key, input: b.input, output: b.output, cache: b.cache, total: b.total, cost: b.cost, calls: b.calls })\n }\n return result\n }\n\n const makeHourly = (map: Map<string, Bucket>): UsageData['hourly'] => {\n const result: UsageData['hourly'] = []\n for (let i = 0; i < 24; i++) {\n const b = map.get(String(i)) ?? emptyBucket()\n result.push({ hour: i, total: b.total, cost: b.cost, calls: b.calls })\n }\n return result\n }\n\n const makeModels = (\n totals: Map<string, Bucket>,\n daysByModel: Map<string, Map<string, Bucket>>,\n hoursByModel: Map<string, Map<string, Bucket>>,\n dayCount: number,\n ): ModelUsage[] => {\n const result: ModelUsage[] = []\n for (const [modelKey, bucket] of totals) {\n const slash = modelKey.indexOf('/')\n const provider = slash < 0 ? '' : modelKey.slice(0, slash)\n const model = slash < 0 ? modelKey : modelKey.slice(slash + 1)\n const days = daysByModel.get(modelKey)\n const hours = hoursByModel.get(modelKey)\n const daily: ModelSeriesPoint[] = makeDaily(days ?? new Map(), dayCount)\n .map(point => ({ total: point.total, cost: point.cost, calls: point.calls }))\n const hourly: ModelSeriesPoint[] = makeHourly(hours ?? new Map())\n .map(point => ({ total: point.total, cost: point.cost, calls: point.calls }))\n result.push({\n provider,\n model,\n input: bucket.input,\n output: bucket.output,\n cache: bucket.cache,\n total: bucket.total,\n cost: bucket.cost,\n calls: bucket.calls,\n daily,\n hourly,\n })\n }\n result.sort((a, b) => b.total - a.total)\n return result\n }\n\n const daily = makeDaily(dayMap, 30)\n const hourly = makeHourly(hourMap)\n\n // 52 full weeks (364 days) rather than a calendar year, so the grid is\n // always a whole number of week-columns and never carries an orphan day.\n const heatmap: UsageData['heatmap'] = []\n for (let i = 363; i >= 0; i--) {\n const key = dayKeyOf(dateBefore(i))\n const b = dayMap.get(key) ?? emptyBucket()\n heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls })\n }\n\n const models = makeModels(modelTotals, modelDays, modelHours, 30)\n const windows = windowAggregates.map(aggregate => ({\n days: aggregate.days,\n daily: makeDaily(aggregate.dayMap, aggregate.days),\n hourly: makeHourly(aggregate.hourMap),\n totals: { ...aggregate.overall },\n models: makeModels(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, aggregate.days),\n sessions: [...aggregate.sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),\n sessionCount: aggregate.sessions.length,\n }))\n\n return {\n ok: true,\n data: {\n daily,\n hourly,\n heatmap,\n models,\n summary: summarize(dayMap, new Date(nowMs)),\n pricing: pricingInfo(nowMs),\n peakSplit,\n sessions: [...sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),\n sessionCount: sessions.length,\n coverage,\n windows,\n totals: {\n input: overall.input,\n output: overall.output,\n cache: overall.cache,\n reasoning: overall.reasoning,\n total: overall.total,\n cost: overall.cost,\n calls: overall.calls,\n cacheSavings: overall.cacheSavings,\n },\n },\n }\n}\n\n/**\n * Usage for exactly one session \u2014 the \u300C\u989D\u5EA6\u300Dtab's own conversation, as\n * opposed to `fetchUsage`'s account-wide replay. Reads a single session log\n * (not every session on disk), so it is cheap enough to call on every tab\n * mount without a TTL memo; the caller is expected to re-fetch whenever the\n * session id it cares about changes.\n */\nexport async function fetchSessionUsage(persistence: SessionPersistenceFace | undefined, sessionId: string): Promise<SessionUsageResponse> {\n if (persistence === undefined) return { ok: false, error: '\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528' }\n if (typeof sessionId !== 'string' || sessionId === '') return { ok: false, error: '\u7F3A\u5C11\u4F1A\u8BDD id' }\n // Defense in depth: src/index.ts already rejects a malformed `id` before\n // this function is ever called (see SESSION_ID_PATTERN in contract.ts),\n // but this function is also unit-tested and reachable directly, so the\n // same check is repeated here rather than trusted to the one caller.\n if (!isValidSessionId(sessionId)) return { ok: false, error: '\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5' }\n\n let events: SessionEventFace[] | undefined\n try {\n const result = await persistence.readFrom(sessionId, 0)\n events = result.events\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n\n const modelTotals = new Map<string, Bucket>()\n let total = 0\n let cost = 0\n let calls = 0\n let firstActive: number | null = null\n let lastActive: number | null = null\n\n addUsageEvent(events, (time, input, output, cache, _reasoning, provider, model) => {\n const modelKey = `${provider}/${model}`\n const eventCost = costOf(time, model, input, cache, output)\n const eventTotal = input + output + cache\n bump(modelTotals, modelKey, input, output, cache, eventCost)\n total += eventTotal\n cost += eventCost\n calls += 1\n if (firstActive === null || time < firstActive) firstActive = time\n if (lastActive === null || time > lastActive) lastActive = time\n })\n\n const models: SessionModelUsage[] = [...modelTotals.entries()]\n .map(([modelKey, bucket]): SessionModelUsage => {\n const slash = modelKey.indexOf('/')\n const provider = slash < 0 ? '' : modelKey.slice(0, slash)\n const model = slash < 0 ? modelKey : modelKey.slice(slash + 1)\n return {\n provider,\n model,\n input: bucket.input,\n output: bucket.output,\n cache: bucket.cache,\n total: bucket.total,\n cost: bucket.cost,\n calls: bucket.calls,\n }\n })\n .sort((a, b) => b.cost - a.cost)\n\n return {\n ok: true,\n data: {\n sessionId,\n title: titleOf(events, sessionId),\n total,\n cost,\n calls,\n firstActive,\n lastActive,\n models,\n },\n }\n}\n", "import type { ServerResponse } from 'node:http'\n\nexport function writeJson(res: ServerResponse, status: number, body: unknown, headers?: Record<string, string>): void {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...headers })\n res.end(JSON.stringify(body))\n}\n", "/**\n * dsh-usage-dashboard host plugin.\n *\n * Registers the package's own fenced JSON API under\n * `/api/dsh-usage-dashboard` (balance + usage + session), consumed by the\n * client half through same-origin fetch. `balance` and `usage` are memoized\n * with a TTL so that tab switches and the widget's polling don't replay\n * session logs or hammer the DeepSeek API; responses carry `Cache-Control`\n * so the browser's HTTP cache serves repeat requests without a network\n * round trip. `?refresh=1` bypasses the memo for an explicit force refresh.\n * `session` (usage for exactly one session, keyed by `?id=`) reads a single\n * session log and is deliberately not memoized \u2014 it is cheap and must\n * reflect that session's live state, not a 5-minute-stale snapshot.\n */\nimport type { HostContext } from './context.ts'\nimport { isValidSessionId } from './contract.ts'\nimport { memoize } from './memo.ts'\nimport { isTrustedApiRequest } from './trust-fence.ts'\nimport { fetchBalance, fetchSessionUsage, fetchUsage } from './usage.ts'\nimport { writeJson } from './wire.ts'\n\n/** Plugin identity for the cordis.patch.yml row (and the client bundle id). */\nexport const name = 'dsh-usage-dashboard'\n\n/** Services required before load: the web server plus the two data sources. */\nexport const inject = ['webServer', 'credentials', 'sessionPersistence']\n\n/** Freshness windows: balance 60s (the widget also polls every 60s), usage\n * 5min (aggregating every session log is the expensive path). */\nconst BALANCE_TTL_MS = 60_000\nconst USAGE_TTL_MS = 5 * 60_000\n\n/**\n * Mount the /api/dsh-usage-dashboard routes.\n * @param ctx - host Cordis context.\n */\nexport function apply(ctx: HostContext): void {\n const balanceMemo = memoize(BALANCE_TTL_MS, () => fetchBalance(ctx.credentials))\n const usageMemo = memoize(USAGE_TTL_MS, () => fetchUsage(ctx.sessionPersistence))\n\n ctx.effect(() => ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dsh-usage-dashboard',\n handler: async (req, res) => {\n if (!isTrustedApiRequest(req)) {\n writeJson(res, 403, { ok: false, error: 'forbidden' })\n return\n }\n if (req.method !== 'GET') {\n writeJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n const url = new URL(req.url ?? '/', 'http://dsh.internal')\n const pathname = url.pathname\n const force = url.searchParams.get('refresh') === '1'\n if (pathname === '/api/dsh-usage-dashboard/balance') {\n const payload = await (force ? balanceMemo.refresh() : balanceMemo.get())\n const headers = payload.ok\n ? { 'cache-control': `private, max-age=${Math.round(BALANCE_TTL_MS / 1000)}` }\n : { 'cache-control': 'no-store' }\n writeJson(res, 200, payload, headers)\n } else if (pathname === '/api/dsh-usage-dashboard/usage') {\n const payload = await (force ? usageMemo.refresh() : usageMemo.get())\n const headers = payload.ok\n ? { 'cache-control': `private, max-age=${Math.round(USAGE_TTL_MS / 1000)}` }\n : { 'cache-control': 'no-store' }\n writeJson(res, 200, payload, headers)\n } else if (pathname === '/api/dsh-usage-dashboard/session') {\n const id = url.searchParams.get('id')\n // `id` arrives verbatim from the browser-controlled `?id=` query\n // parameter (unlike `usage`'s session ids, which the host itself\n // enumerates via `persistence.list()`), so a malformed value is\n // rejected here \u2014 before it ever reaches `persistence.readFrom` \u2014\n // rather than trusted to fetchSessionUsage's own defense-in-depth\n // check. Reject as early as the route layer allows.\n if (id === null || id.trim() === '' || !isValidSessionId(id)) {\n // A missing/blank/malformed `id` is the caller's mistake (bad\n // input), so it gets a real 4xx; any other failure (e.g. the log\n // itself unreadable) keeps the same \"200 with ok:false\" shape as\n // balance/usage so the client's one error-handling path covers all\n // three endpoints.\n const error = id === null || id.trim() === '' ? '\u7F3A\u5C11\u4F1A\u8BDD id' : '\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5'\n writeJson(res, 400, { ok: false, error }, { 'cache-control': 'no-store' })\n return\n }\n const payload = await fetchSessionUsage(ctx.sessionPersistence, id)\n // id is already known-well-formed here, so any remaining failure is\n // downstream (e.g. the log itself unreadable) and keeps the same\n // \"200 with ok:false\" shape as balance/usage.\n writeJson(res, 200, payload, { 'cache-control': 'no-store' })\n } else {\n writeJson(res, 404, { ok: false, error: 'not found' })\n }\n },\n }), 'dsh-usage-dashboard: /api routes')\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/** Wire contracts shared by the host API and the client fetchers. */\n\nexport interface BalanceInfo {\n currency: string\n total: string\n granted: string\n toppedUp: string\n}\n\nexport interface BalanceData {\n isAvailable: boolean\n balances: BalanceInfo[]\n}\n\nexport interface BalanceResponse {\n ok: boolean\n error?: string\n data?: BalanceData\n}\n\nexport interface DailyUsage {\n date: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\nexport interface HourlyUsage {\n hour: number\n total: number\n cost: number\n calls: number\n}\n\nexport interface HeatmapUsage {\n date: string\n total: number\n cost: number\n calls: number\n}\n\nexport interface UsageTotals {\n input: number\n output: number\n cache: number\n reasoning: number\n total: number\n cost: number\n calls: number\n /**\n * What the cache-hit tokens would have cost at cache-miss rates, minus what\n * they actually cost \u2014 i.e. the money prefix caching saved. Folded per\n * record so it respects each record's model and time-of-day rates.\n */\n cacheSavings: number\n}\n\n/** One per-day / per-hour point of a single model's series. */\nexport interface ModelSeriesPoint {\n total: number\n cost: number\n calls: number\n}\n\n/** Per-model usage aggregation: overall totals plus 30-day and 24-hour series. */\nexport interface ModelUsage {\n provider: string\n model: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n daily: ModelSeriesPoint[]\n hourly: ModelSeriesPoint[]\n}\n\n/** Usage rolled up over one time window. */\nexport interface PeriodUsage {\n total: number\n cost: number\n calls: number\n}\n\n/**\n * Time-anchored rollups. \"Accumulated since forever\" answers no question a\n * user actually has; these do: what did today cost, what has this month cost,\n * and is that more or less than the comparable window before it.\n */\nexport interface UsageSummary {\n today: PeriodUsage\n yesterday: PeriodUsage\n /** Current calendar month, to date. */\n month: PeriodUsage\n /** Previous calendar month up to the same day-of-month, so a mid-month\n * comparison is against a same-length window rather than a full month. */\n lastMonthToDate: PeriodUsage\n}\n\n/** CNY per 1M tokens for one model tier. */\nexport interface PricingRates {\n cacheHit: number\n input: number\n output: number\n}\n\nexport interface PricingTier {\n model: string\n peak: PricingRates\n /** Null while flat pricing is in effect (before the peak/off-peak switch). */\n offPeak: PricingRates | null\n}\n\n/** What the cost column was computed with, so the estimate can be audited. */\nexport interface PricingInfo {\n currency: string\n /** Date peak/off-peak pricing takes effect. */\n switchDate: string\n splitActive: boolean\n inPeakNow: boolean\n peakWindows: string[]\n tiers: PricingTier[]\n}\n\n/**\n * How usage divides between DeepSeek's peak and off-peak windows, plus the\n * same usage re-priced two ways \u2014 so the 2026-08-17 change can be answered\n * before it lands (\"what will this cost me\") and after (\"what would shifting\n * off-peak save me\").\n */\nexport interface PeakSplit {\n peak: PeriodUsage\n offPeak: PeriodUsage\n /** Every record priced under the peak/off-peak table, as actually classified. */\n peakEraCost: number\n /** Every record priced under that table as if it had landed off-peak. */\n offPeakEraCost: number\n}\n\n/** One session's share of the bill. */\nexport interface SessionCost {\n id: string\n /** The session's title from its log; falls back to a short id. */\n title: string\n total: number\n cost: number\n calls: number\n /** Epoch ms of the last usage record in this session. */\n lastActive: number\n}\n\n/**\n * What the local log replay could actually inspect. Totals can otherwise look\n * authoritative even when one corrupt session was skipped, so this metadata\n * travels with every usage response and lets the UI disclose partial results.\n */\nexport interface UsageCoverage {\n scope: 'local-dsh-session-logs'\n /** Session headers with a usable id returned by the local persistence. */\n listedSessions: number\n /** Session logs read successfully, including logs with no usage. */\n scannedSessions: number\n /** Valid assistant usage records folded into the totals. */\n usageRecords: number\n /** Usage records rejected because their time or token data was invalid. */\n skippedRecords: number\n /** Session logs that could not be read at all. */\n failedSessions: number\n /** Epoch ms of the first/last valid usage record across all local logs. */\n earliestAt: number | null\n latestAt: number | null\n}\n\nexport const USAGE_WINDOW_DAYS = [7, 30, 90, 365] as const\nexport type UsageWindowDays = (typeof USAGE_WINDOW_DAYS)[number]\n\n/**\n * Session ids observed in this codebase (host-issued, e.g.\n * `session-484a1c14-c6fe-4d6a-abfd-a2d8d2f664d5`) are always plain\n * ASCII-safe tokens \u2014 no documented format contract exists for them outside\n * this repo (`SessionPersistenceFace`/`readFrom` is implemented by the DSH\n * host, not here), so this is a defensive charset whitelist rather than an\n * exact-format check: letters, digits, `-`, `_` only, with a generous length\n * cap. It exists specifically to stop path-separator / `..` / null-byte\n * payloads from ever reaching `persistence.readFrom(id, 0)` \u2014 `id` on the\n * `/session` route arrives verbatim from the browser-controlled `?id=` query\n * parameter, unlike `fetchUsage()`'s session ids, which the host itself\n * enumerates via `persistence.list()`.\n */\nexport const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\nexport const isValidSessionId = (id: string): boolean => SESSION_ID_PATTERN.test(id)\n\n/** One internally consistent view of the same local logs over a calendar window. */\nexport interface UsageWindow {\n days: UsageWindowDays\n daily: DailyUsage[]\n hourly: HourlyUsage[]\n totals: UsageTotals\n models: ModelUsage[]\n /** Most expensive sessions inside this window, capped at SESSION_TOP_N. */\n sessions: SessionCost[]\n /** All sessions that contributed usage inside this window. */\n sessionCount: number\n}\n\nexport interface UsageData {\n daily: DailyUsage[]\n hourly: HourlyUsage[]\n heatmap: HeatmapUsage[]\n totals: UsageTotals\n models: ModelUsage[]\n summary: UsageSummary\n pricing: PricingInfo\n peakSplit: PeakSplit\n /** Most expensive sessions first, capped \u2014 see SESSION_TOP_N. */\n sessions: SessionCost[]\n /** How many sessions contributed usage in total. */\n sessionCount: number\n coverage: UsageCoverage\n windows: UsageWindow[]\n}\n\nexport interface UsageResponse {\n ok: boolean\n error?: string\n data?: UsageData\n}\n\n/**\n * One model's contribution to a single session's bill \u2014 the same field\n * shape as `ModelUsage` minus the `daily`/`hourly` series, which only make\n * sense aggregated across many sessions.\n */\nexport interface SessionModelUsage {\n provider: string\n model: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\n/** One completed conversation turn's usage. `messageId` is the durable id of\n * the turn's final assistant message \u2014 the same identity the chat action-strip\n * slot exposes to client plugins. Tool-call steps inside the turn are folded\n * into this one figure rather than shown as misleading separate charges. */\nexport interface TurnUsage {\n messageId: string\n total: number\n cost: number\n calls: number\n}\n\n/**\n * Usage for exactly one session \u2014 what the \u300C\u989D\u5EA6\u300Dtab's own conversation\n * is costing, as opposed to `UsageData`'s account-wide totals. Fetched on\n * demand (see `fetchSessionUsage`), not part of the 5-minute `usage` memo:\n * a single session read is cheap and must reflect the session's live state.\n */\nexport interface SessionUsageData {\n sessionId: string\n /** The session's own title from its log; falls back to a short id. */\n title: string\n total: number\n cost: number\n calls: number\n /** Epoch ms of the first/last valid usage record in this session, or null\n * when the session has produced no usage yet. */\n firstActive: number | null\n lastActive: number | null\n /** Most expensive model first. */\n models: SessionModelUsage[]\n /** Completed turns, in log order, addressed by their final message id. */\n turns: TurnUsage[]\n}\n\nexport interface SessionUsageResponse {\n ok: boolean\n error?: string\n data?: SessionUsageData\n}\n", "/**\n * Tiny TTL memo for the /api routes.\n *\n * The usage endpoint replays every session log, so it must not run on every\n * tab click; the balance endpoint also calls the DeepSeek API. The memo:\n *\n * - `get()` serves a successful cached response while it is within `ttlMs`,\n * otherwise recomputes;\n * - `refresh()` recomputes unconditionally and reseeds the cache;\n * - concurrent recomputes (of either kind) are deduped into one upstream\n * call;\n * - `ok:false` responses are never cached, so a transient failure is retried\n * on the next request.\n *\n * No timers are held, so nothing needs disposal on plugin unload.\n */\n\ninterface Entry<T> {\n value: T\n setAt: number\n}\n\ninterface MemoState<T> {\n entry: Entry<T> | undefined\n inflight: Promise<T> | undefined\n}\n\nexport interface Memo<T> {\n get(): Promise<T>\n refresh(): Promise<T>\n}\n\nexport function memoize<T extends { ok: boolean }>(ttlMs: number, compute: () => Promise<T>): Memo<T> {\n const state: MemoState<T> = { entry: undefined, inflight: undefined }\n\n const run = (): Promise<T> => {\n if (state.inflight !== undefined) return state.inflight\n const task = compute().then(\n (value) => {\n state.inflight = undefined\n if (value.ok) state.entry = { value, setAt: Date.now() }\n return value\n },\n (err: unknown) => {\n state.inflight = undefined\n throw err\n },\n )\n state.inflight = task\n return task\n }\n\n return {\n get: () => {\n const hit = state.entry\n if (hit !== undefined && hit.value.ok && Date.now() - hit.setAt < ttlMs) {\n return Promise.resolve(hit.value)\n }\n return run()\n },\n refresh: () => run(),\n }\n}\n", "/**\n * Browser-trust fence for the plugin's own /api route \u2014 the same fences the\n * harness's /api gateway applies: the Host header must name a loopback\n * authority, a browser-marked cross-site request is refused, and an attached\n * Origin must be exactly this authority.\n */\nimport type { IncomingMessage } from 'node:http'\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]'\n}\n\nexport function isTrustedApiRequest(req: IncomingMessage): boolean {\n const host = req.headers.host\n if (host === undefined) return false\n let hostUrl: URL\n try {\n hostUrl = new URL(`http://${host}`)\n } catch {\n return false\n }\n if (!isLoopbackHostname(hostUrl.hostname)) return false\n if (req.headers['sec-fetch-site'] === 'cross-site') return false\n const origin = req.headers.origin\n if (origin === undefined) return true\n try {\n return new URL(origin).host === hostUrl.host\n } catch {\n return false\n }\n}\n", "/**\n * DeepSeek API price table and cost estimation.\n *\n * Two things the previous flat constant got wrong: every model was billed at\n * deepseek-v4-pro rates (flash is ~3x cheaper), and the 2026-08-17 switch to\n * peak/off-peak pricing was not modelled at all.\n *\n * Rates are CNY per 1M tokens, from DeepSeek's 2026-08-13 price announcement.\n * Peak window is Beijing time 09:00\u201312:00 and 14:00\u201318:00; off-peak is half of\n * peak. Usage recorded before the switch is still costed at the old flat rates,\n * so historical days keep the price that was actually charged.\n *\n * Editing this table is the one place to touch when DeepSeek changes prices.\n */\nimport type { PricingInfo, PricingRates } from './contract.ts'\n\n/** 2026-08-17 00:00 Beijing time (UTC+8), when peak/off-peak pricing starts. */\nexport const PEAK_PRICING_FROM_MS = Date.UTC(2026, 7, 16, 16, 0, 0)\n\n/** Peak hours in Beijing time, as [startHour, endHour) pairs. */\nconst PEAK_WINDOWS: Array<[number, number]> = [[9, 12], [14, 18]]\n\nconst BEIJING_OFFSET_MS = 8 * 3_600_000\n\ntype Tier = 'pro' | 'flash'\n\n/** Flat rates in effect until 2026-08-17. */\nconst LEGACY_RATES: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.025, input: 3, output: 6 },\n flash: { cacheHit: 0.02, input: 1, output: 2 },\n}\n\n/** Peak rates from 2026-08-17; off-peak is exactly half of each. */\nconst PEAK_RATES: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.3, input: 9, output: 27 },\n flash: { cacheHit: 0.1, input: 3, output: 9 },\n}\n\nconst halved = (rates: PricingRates): PricingRates => ({\n cacheHit: rates.cacheHit / 2,\n input: rates.input / 2,\n output: rates.output / 2,\n})\n\n/** Which price tier a model name falls into; anything unrecognised is billed\n * as pro, the conservative (more expensive) guess. */\nexport function tierOf(model: string): Tier {\n return model.toLowerCase().includes('flash') ? 'flash' : 'pro'\n}\n\n/** Whether a moment falls in a peak window, judged in Beijing time so the\n * estimate does not drift with the machine's timezone. */\nexport function isPeak(timeMs: number): boolean {\n const hour = Math.floor((((timeMs + BEIJING_OFFSET_MS) % 86_400_000) + 86_400_000) % 86_400_000 / 3_600_000)\n return PEAK_WINDOWS.some(([from, to]) => hour >= from && hour < to)\n}\n\n/** Rates applying to one model at one moment. */\nexport function ratesAt(timeMs: number, model: string): PricingRates {\n const tier = tierOf(model)\n if (timeMs < PEAK_PRICING_FROM_MS) return LEGACY_RATES[tier]\n const peak = PEAK_RATES[tier]\n return isPeak(timeMs) ? peak : halved(peak)\n}\n\n/**\n * What prefix caching saved on one record: the cache-hit tokens re-priced at\n * the cache-miss rate, minus what they actually cost.\n */\nexport function cacheSavingOf(timeMs: number, model: string, cache: number): number {\n const rates = ratesAt(timeMs, model)\n return (cache * (rates.input - rates.cacheHit)) / 1_000_000\n}\n\nconst applyRates = (rates: PricingRates, input: number, cache: number, output: number): number =>\n (input * rates.input + cache * rates.cacheHit + output * rates.output) / 1_000_000\n\n/** Estimated CNY cost of one usage record. */\nexport function costOf(timeMs: number, model: string, input: number, cache: number, output: number): number {\n return applyRates(ratesAt(timeMs, model), input, cache, output)\n}\n\n/**\n * The same record priced under the peak/off-peak table regardless of when it\n * happened \u2014 `forceOffPeak` prices it as if it had landed in an idle window.\n * Used to answer \"what will the new pricing cost me\" before the switch and\n * \"what would shifting off-peak save\" after it.\n */\nexport function costUnderPeakEra(timeMs: number, model: string, input: number, cache: number, output: number, forceOffPeak = false): number {\n const peak = PEAK_RATES[tierOf(model)]\n const rates = !forceOffPeak && isPeak(timeMs) ? peak : halved(peak)\n return applyRates(rates, input, cache, output)\n}\n\n/** The price table to show the user, describing whatever is in effect now. */\nexport function pricingInfo(nowMs: number): PricingInfo {\n const split = nowMs >= PEAK_PRICING_FROM_MS\n return {\n currency: 'CNY',\n switchDate: '2026-08-17',\n splitActive: split,\n inPeakNow: split && isPeak(nowMs),\n peakWindows: ['09:00\u201312:00', '14:00\u201318:00'],\n tiers: (['pro', 'flash'] as Tier[]).map(tier => ({\n model: tier === 'pro' ? 'deepseek-v4-pro' : 'deepseek-v4-flash',\n peak: split ? PEAK_RATES[tier] : LEGACY_RATES[tier],\n offPeak: split ? halved(PEAK_RATES[tier]) : null,\n })),\n }\n}\n", "/**\n * Balance and usage data sources.\n *\n * - Balance: DeepSeek API `GET /user/balance` with the key resolved through\n * the credentials service (`DEEPSEEK_API_KEY`).\n * - Usage: token usage folded from the DSH session logs (`sessionPersistence`),\n * one record per `assistant/message` event, bucketed by local day / hour.\n */\nimport { isValidSessionId, USAGE_WINDOW_DAYS } from './contract.ts'\nimport type { BalanceResponse, ModelSeriesPoint, ModelUsage, PeakSplit, PeriodUsage, SessionCost, SessionModelUsage, SessionUsageResponse, UsageCoverage, UsageData, UsageResponse, UsageSummary, UsageWindowDays } from './contract.ts'\nimport type { CredentialsFace, SessionEventFace, SessionPersistenceFace } from './context.ts'\nimport { cacheSavingOf, costOf, costUnderPeakEra, isPeak, pricingInfo } from './pricing.ts'\n\nconst pad2 = (n: number): string => (n < 10 ? `0${n}` : String(n))\n\nfunction errorMessage(err: unknown): string {\n return (err as { message?: string } | null)?.message ?? String(err)\n}\n\nexport async function fetchBalance(credentials: CredentialsFace | undefined): Promise<BalanceResponse> {\n if (credentials === undefined) return { ok: false, error: '\u51ED\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528' }\n let cred: { value: string } | undefined\n try {\n cred = await credentials.resolve('DEEPSEEK_API_KEY')\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6 API Key \u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n if (cred === undefined || cred.value === '') {\n return { ok: false, error: '\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09' }\n }\n let res: Response\n try {\n res = await fetch('https://api.deepseek.com/user/balance', {\n headers: { authorization: `Bearer ${cred.value}` },\n signal: AbortSignal.timeout(20000),\n })\n } catch (err) {\n return { ok: false, error: `\u8BF7\u6C42\u4F59\u989D\u63A5\u53E3\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n if (!res.ok) return { ok: false, error: `\u4F59\u989D\u63A5\u53E3\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09` }\n let parsed: unknown\n try {\n parsed = await res.json()\n } catch {\n return { ok: false, error: '\u89E3\u6790\u4F59\u989D\u54CD\u5E94\u5931\u8D25' }\n }\n const p = parsed as {\n is_available?: boolean\n balance_infos?: Array<{ currency?: string; total_balance?: string; granted_balance?: string; topped_up_balance?: string }>\n }\n return {\n ok: true,\n data: {\n isAvailable: p.is_available === true,\n balances: (p.balance_infos ?? []).map(b => ({\n currency: b.currency ?? '',\n total: b.total_balance ?? '0',\n granted: b.granted_balance ?? '0',\n toppedUp: b.topped_up_balance ?? '0',\n })),\n },\n }\n}\n\ninterface Bucket {\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\nconst emptyBucket = (): Bucket => ({ input: 0, output: 0, cache: 0, total: 0, cost: 0, calls: 0 })\n\n/** Cost is computed once per event (it depends on the model and on whether\n * the event landed in a peak window) and folded into every bucket that\n * needs it \u2014 shared by the account-wide replay and the single-session one. */\nfunction bump(map: Map<string, Bucket>, key: string, input: number, output: number, cache: number, cost: number): void {\n const bucket = map.get(key) ?? emptyBucket()\n bucket.input += input\n bucket.output += output\n bucket.cache += cache\n // reasoningTokens is a subset of outputTokens; never double-count it.\n bucket.total += input + output + cache\n bucket.cost += cost\n bucket.calls += 1\n map.set(key, bucket)\n}\n\ntype OverallBucket = Bucket & { reasoning: number; cacheSavings: number }\nconst emptyOverall = (): OverallBucket => ({ ...emptyBucket(), reasoning: 0, cacheSavings: 0 })\n\ninterface WindowAccumulator {\n days: UsageWindowDays\n startKey: string\n endKey: string\n dayMap: Map<string, Bucket>\n hourMap: Map<string, Bucket>\n overall: OverallBucket\n modelTotals: Map<string, Bucket>\n modelDays: Map<string, Map<string, Bucket>>\n modelHours: Map<string, Map<string, Bucket>>\n sessions: SessionCost[]\n}\n\n/** How many sessions the ranking keeps; the rest are summarised by count. */\nconst SESSION_TOP_N = 6\n\n/**\n * The session's own title, folded from its log: `session/title` events are\n * append-only and the last one wins. It rides along with the usage replay, so\n * naming a session costs no extra read.\n */\nfunction titleOf(events: SessionEventFace[] | undefined, id: string): string {\n let title = ''\n if (events !== undefined) {\n for (const ev of events) {\n if (ev?.type !== 'session/title') continue\n const next = ev.data?.title\n if (typeof next === 'string' && next !== '') title = next\n }\n }\n return title !== '' ? title : `\u4F1A\u8BDD ${id.slice(0, 8)}`\n}\n\nconst dayKeyOf = (d: Date): string => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n\n/**\n * Roll the per-day buckets up into the windows the dashboard's summary card\n * shows. Everything is keyed by local date, matching how the buckets were\n * filled, so \"today\" means the user's today.\n */\nfunction summarize(dayMap: Map<string, Bucket>, now: Date): UsageSummary {\n const sum = (matches: (key: string) => boolean): PeriodUsage => {\n const acc: PeriodUsage = { total: 0, cost: 0, calls: 0 }\n for (const [key, bucket] of dayMap) {\n if (!matches(key)) continue\n acc.total += bucket.total\n acc.cost += bucket.cost\n acc.calls += bucket.calls\n }\n return acc\n }\n const todayKey = dayKeyOf(now)\n const yesterdayKey = dayKeyOf(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1))\n const monthPrefix = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-`\n const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)\n const lastMonthPrefix = `${lastMonth.getFullYear()}-${pad2(lastMonth.getMonth() + 1)}-`\n const dayOfMonth = now.getDate()\n return {\n today: sum(key => key === todayKey),\n yesterday: sum(key => key === yesterdayKey),\n month: sum(key => key.startsWith(monthPrefix)),\n lastMonthToDate: sum(key => key.startsWith(lastMonthPrefix) && Number(key.slice(8)) <= dayOfMonth),\n }\n}\n\n/**\n * Fold usage out of one session's event log. The model for every\n * `assistant/message` usage record is the one from the latest preceding\n * `request/header` event (each request logs one before dispatch), so usage\n * can be attributed per model. Events without any preceding header fall back\n * to the `''`/`''` (unknown) bucket.\n */\nfunction addUsageEvent(\n events: SessionEventFace[] | undefined,\n onEvent: (\n time: number,\n input: number,\n output: number,\n cache: number,\n reasoning: number,\n provider: string,\n model: string,\n messageId: string | undefined,\n turn: number | undefined,\n ) => void,\n): Pick<UsageCoverage, 'usageRecords' | 'skippedRecords'> {\n const result = { usageRecords: 0, skippedRecords: 0 }\n if (events === undefined) return result\n let provider = ''\n let model = ''\n for (const ev of events) {\n if (ev?.type === 'request/header') {\n const cfg = ev.data?.header?.config\n if (cfg?.provider !== undefined && cfg?.model !== undefined) {\n provider = cfg.provider\n model = cfg.model\n }\n continue\n }\n if (ev?.type !== 'assistant/message') continue\n const usage = ev.data?.usage\n if (usage === undefined) continue\n const values = [usage.inputTokens, usage.outputTokens, usage.cacheReadTokens, usage.reasoningTokens]\n .map(value => value === undefined ? 0 : Number(value))\n if (typeof ev.time !== 'number' || !Number.isFinite(ev.time) || values.some(value => !Number.isFinite(value) || value < 0)) {\n result.skippedRecords += 1\n continue\n }\n const [input = 0, output = 0, cache = 0, reasoning = 0] = values\n const rawMessageId = ev.data?.message?.id\n const messageId = typeof rawMessageId === 'string' && rawMessageId !== '' ? rawMessageId : undefined\n const rawTurn = ev.data?.turn\n const turn = typeof rawTurn === 'number' && Number.isFinite(rawTurn) ? rawTurn : undefined\n onEvent(ev.time, input, output, cache, reasoning, provider, model, messageId, turn)\n result.usageRecords += 1\n }\n return result\n}\n\nexport async function fetchUsage(persistence: SessionPersistenceFace | undefined, nowMs = Date.now()): Promise<UsageResponse> {\n if (persistence === undefined) return { ok: false, error: '\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528' }\n let headers: Array<{ id?: string; sessionId?: string }>\n try {\n headers = await persistence.list()\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n const dayMap = new Map<string, Bucket>()\n const hourMap = new Map<string, Bucket>()\n const overall = emptyOverall()\n // Per model: key `${provider}/${model}` -> totals and day/hour maps.\n const modelTotals = new Map<string, Bucket>()\n const modelDays = new Map<string, Map<string, Bucket>>()\n const modelHours = new Map<string, Map<string, Bucket>>()\n const sessions: SessionCost[] = []\n const coverage: UsageCoverage = {\n scope: 'local-dsh-session-logs',\n listedSessions: 0,\n scannedSessions: 0,\n usageRecords: 0,\n skippedRecords: 0,\n failedSessions: 0,\n earliestAt: null,\n latestAt: null,\n }\n // Peak / off-peak split, plus the same usage re-priced under the new table.\n const peakSplit: PeakSplit = {\n peak: { total: 0, cost: 0, calls: 0 },\n offPeak: { total: 0, cost: 0, calls: 0 },\n peakEraCost: 0,\n offPeakEraCost: 0,\n }\n\n const now = new Date(nowMs)\n const dateBefore = (days: number): Date => new Date(now.getFullYear(), now.getMonth(), now.getDate() - days)\n const windowAggregates: WindowAccumulator[] = USAGE_WINDOW_DAYS.map(days => ({\n days,\n startKey: dayKeyOf(dateBefore(days - 1)),\n endKey: dayKeyOf(now),\n dayMap: new Map(),\n hourMap: new Map(),\n overall: emptyOverall(),\n modelTotals: new Map(),\n modelDays: new Map(),\n modelHours: new Map(),\n sessions: [],\n }))\n\n const bumpOverall = (\n bucket: OverallBucket,\n input: number,\n output: number,\n cache: number,\n reasoning: number,\n cost: number,\n cacheSavings: number,\n ): void => {\n bucket.input += input\n bucket.output += output\n bucket.cache += cache\n bucket.reasoning += reasoning\n bucket.total += input + output + cache\n bucket.cost += cost\n bucket.cacheSavings += cacheSavings\n bucket.calls += 1\n }\n\n const bumpModel = (\n totals: Map<string, Bucket>,\n days: Map<string, Map<string, Bucket>>,\n hours: Map<string, Map<string, Bucket>>,\n modelKey: string,\n dayKey: string,\n hourKey: string,\n input: number,\n output: number,\n cache: number,\n cost: number,\n ): void => {\n bump(totals, modelKey, input, output, cache, cost)\n let dayBuckets = days.get(modelKey)\n if (dayBuckets === undefined) {\n dayBuckets = new Map()\n days.set(modelKey, dayBuckets)\n }\n bump(dayBuckets, dayKey, input, output, cache, cost)\n let hourBuckets = hours.get(modelKey)\n if (hourBuckets === undefined) {\n hourBuckets = new Map()\n hours.set(modelKey, hourBuckets)\n }\n bump(hourBuckets, hourKey, input, output, cache, cost)\n }\n\n const bumpSession = (session: SessionCost, time: number, total: number, cost: number): void => {\n session.total += total\n session.cost += cost\n session.calls += 1\n if (time > session.lastActive) session.lastActive = time\n }\n\n const sessionIds = headers\n .map(header => header.id ?? header.sessionId)\n .filter((sid): sid is string => sid !== undefined && sid !== '')\n coverage.listedSessions = sessionIds.length\n\n for (const sid of sessionIds) {\n try {\n const { events } = await persistence.readFrom(sid, 0)\n coverage.scannedSessions += 1\n const session: SessionCost = { id: sid, title: '', total: 0, cost: 0, calls: 0, lastActive: 0 }\n const windowSessions = windowAggregates.map((): SessionCost => ({ id: sid, title: '', total: 0, cost: 0, calls: 0, lastActive: 0 }))\n const scanned = addUsageEvent(events, (time, input, output, cache, reasoning, provider, model) => {\n if (coverage.earliestAt === null || time < coverage.earliestAt) coverage.earliestAt = time\n if (coverage.latestAt === null || time > coverage.latestAt) coverage.latestAt = time\n const d = new Date(time)\n const dayKey = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n const hourKey = String(d.getHours())\n const modelKey = `${provider}/${model}`\n const cost = costOf(time, model, input, cache, output)\n const cacheSavings = cacheSavingOf(time, model, cache)\n const eventTotal = input + output + cache\n bump(dayMap, dayKey, input, output, cache, cost)\n bump(hourMap, hourKey, input, output, cache, cost)\n bumpModel(modelTotals, modelDays, modelHours, modelKey, dayKey, hourKey, input, output, cache, cost)\n bumpOverall(overall, input, output, cache, reasoning, cost, cacheSavings)\n bumpSession(session, time, eventTotal, cost)\n for (let i = 0; i < windowAggregates.length; i++) {\n const aggregate = windowAggregates[i]\n if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue\n bump(aggregate.dayMap, dayKey, input, output, cache, cost)\n bump(aggregate.hourMap, hourKey, input, output, cache, cost)\n bumpModel(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, modelKey, dayKey, hourKey, input, output, cache, cost)\n bumpOverall(aggregate.overall, input, output, cache, reasoning, cost, cacheSavings)\n bumpSession(windowSessions[i], time, eventTotal, cost)\n }\n const window = isPeak(time) ? peakSplit.peak : peakSplit.offPeak\n window.total += eventTotal\n window.cost += cost\n window.calls += 1\n peakSplit.peakEraCost += costUnderPeakEra(time, model, input, cache, output)\n peakSplit.offPeakEraCost += costUnderPeakEra(time, model, input, cache, output, true)\n })\n coverage.usageRecords += scanned.usageRecords\n coverage.skippedRecords += scanned.skippedRecords\n if (session.calls > 0) {\n const title = titleOf(events, sid)\n session.title = title\n sessions.push(session)\n for (let i = 0; i < windowAggregates.length; i++) {\n const windowSession = windowSessions[i]\n if (windowSession.calls === 0) continue\n windowSession.title = title\n windowAggregates[i].sessions.push(windowSession)\n }\n }\n } catch {\n // One broken session log must not sink the whole dashboard.\n coverage.failedSessions += 1\n }\n }\n\n const makeDaily = (map: Map<string, Bucket>, count: number): UsageData['daily'] => {\n const result: UsageData['daily'] = []\n for (let i = count - 1; i >= 0; i--) {\n const key = dayKeyOf(dateBefore(i))\n const b = map.get(key) ?? emptyBucket()\n result.push({ date: key, input: b.input, output: b.output, cache: b.cache, total: b.total, cost: b.cost, calls: b.calls })\n }\n return result\n }\n\n const makeHourly = (map: Map<string, Bucket>): UsageData['hourly'] => {\n const result: UsageData['hourly'] = []\n for (let i = 0; i < 24; i++) {\n const b = map.get(String(i)) ?? emptyBucket()\n result.push({ hour: i, total: b.total, cost: b.cost, calls: b.calls })\n }\n return result\n }\n\n const makeModels = (\n totals: Map<string, Bucket>,\n daysByModel: Map<string, Map<string, Bucket>>,\n hoursByModel: Map<string, Map<string, Bucket>>,\n dayCount: number,\n ): ModelUsage[] => {\n const result: ModelUsage[] = []\n for (const [modelKey, bucket] of totals) {\n const slash = modelKey.indexOf('/')\n const provider = slash < 0 ? '' : modelKey.slice(0, slash)\n const model = slash < 0 ? modelKey : modelKey.slice(slash + 1)\n const days = daysByModel.get(modelKey)\n const hours = hoursByModel.get(modelKey)\n const daily: ModelSeriesPoint[] = makeDaily(days ?? new Map(), dayCount)\n .map(point => ({ total: point.total, cost: point.cost, calls: point.calls }))\n const hourly: ModelSeriesPoint[] = makeHourly(hours ?? new Map())\n .map(point => ({ total: point.total, cost: point.cost, calls: point.calls }))\n result.push({\n provider,\n model,\n input: bucket.input,\n output: bucket.output,\n cache: bucket.cache,\n total: bucket.total,\n cost: bucket.cost,\n calls: bucket.calls,\n daily,\n hourly,\n })\n }\n result.sort((a, b) => b.total - a.total)\n return result\n }\n\n const daily = makeDaily(dayMap, 30)\n const hourly = makeHourly(hourMap)\n\n // 52 full weeks (364 days) rather than a calendar year, so the grid is\n // always a whole number of week-columns and never carries an orphan day.\n const heatmap: UsageData['heatmap'] = []\n for (let i = 363; i >= 0; i--) {\n const key = dayKeyOf(dateBefore(i))\n const b = dayMap.get(key) ?? emptyBucket()\n heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls })\n }\n\n const models = makeModels(modelTotals, modelDays, modelHours, 30)\n const windows = windowAggregates.map(aggregate => ({\n days: aggregate.days,\n daily: makeDaily(aggregate.dayMap, aggregate.days),\n hourly: makeHourly(aggregate.hourMap),\n totals: { ...aggregate.overall },\n models: makeModels(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, aggregate.days),\n sessions: [...aggregate.sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),\n sessionCount: aggregate.sessions.length,\n }))\n\n return {\n ok: true,\n data: {\n daily,\n hourly,\n heatmap,\n models,\n summary: summarize(dayMap, new Date(nowMs)),\n pricing: pricingInfo(nowMs),\n peakSplit,\n sessions: [...sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),\n sessionCount: sessions.length,\n coverage,\n windows,\n totals: {\n input: overall.input,\n output: overall.output,\n cache: overall.cache,\n reasoning: overall.reasoning,\n total: overall.total,\n cost: overall.cost,\n calls: overall.calls,\n cacheSavings: overall.cacheSavings,\n },\n },\n }\n}\n\n/**\n * Usage for exactly one session \u2014 the \u300C\u989D\u5EA6\u300Dtab's own conversation, as\n * opposed to `fetchUsage`'s account-wide replay. Reads a single session log\n * (not every session on disk), so it is cheap enough to call on every tab\n * mount without a TTL memo; the caller is expected to re-fetch whenever the\n * session id it cares about changes.\n */\nexport async function fetchSessionUsage(persistence: SessionPersistenceFace | undefined, sessionId: string): Promise<SessionUsageResponse> {\n if (persistence === undefined) return { ok: false, error: '\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528' }\n if (typeof sessionId !== 'string' || sessionId === '') return { ok: false, error: '\u7F3A\u5C11\u4F1A\u8BDD id' }\n // Defense in depth: src/index.ts already rejects a malformed `id` before\n // this function is ever called (see SESSION_ID_PATTERN in contract.ts),\n // but this function is also unit-tested and reachable directly, so the\n // same check is repeated here rather than trusted to the one caller.\n if (!isValidSessionId(sessionId)) return { ok: false, error: '\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5' }\n\n let events: SessionEventFace[] | undefined\n try {\n const result = await persistence.readFrom(sessionId, 0)\n events = result.events\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n\n const modelTotals = new Map<string, Bucket>()\n let total = 0\n let cost = 0\n let calls = 0\n let firstActive: number | null = null\n let lastActive: number | null = null\n const turnTotals = new Map<string, { messageId: string; total: number; cost: number; calls: number }>()\n\n addUsageEvent(events, (time, input, output, cache, _reasoning, provider, model, messageId, turn) => {\n const modelKey = `${provider}/${model}`\n const eventCost = costOf(time, model, input, cache, output)\n const eventTotal = input + output + cache\n bump(modelTotals, modelKey, input, output, cache, eventCost)\n total += eventTotal\n cost += eventCost\n calls += 1\n if (firstActive === null || time < firstActive) firstActive = time\n if (lastActive === null || time > lastActive) lastActive = time\n if (messageId !== undefined) {\n // A turn may make several model calls while tools run. The chat renders\n // one action strip beside its final assistant message, so keep replacing\n // the address while accumulating every step's usage under the turn id.\n const key = turn === undefined ? `message:${messageId}` : `turn:${turn}`\n const aggregate = turnTotals.get(key) ?? { messageId, total: 0, cost: 0, calls: 0 }\n aggregate.messageId = messageId\n aggregate.total += eventTotal\n aggregate.cost += eventCost\n aggregate.calls += 1\n turnTotals.set(key, aggregate)\n }\n })\n\n const models: SessionModelUsage[] = [...modelTotals.entries()]\n .map(([modelKey, bucket]): SessionModelUsage => {\n const slash = modelKey.indexOf('/')\n const provider = slash < 0 ? '' : modelKey.slice(0, slash)\n const model = slash < 0 ? modelKey : modelKey.slice(slash + 1)\n return {\n provider,\n model,\n input: bucket.input,\n output: bucket.output,\n cache: bucket.cache,\n total: bucket.total,\n cost: bucket.cost,\n calls: bucket.calls,\n }\n })\n .sort((a, b) => b.cost - a.cost)\n\n return {\n ok: true,\n data: {\n sessionId,\n title: titleOf(events, sessionId),\n total,\n cost,\n calls,\n firstActive,\n lastActive,\n models,\n turns: [...turnTotals.values()],\n },\n }\n}\n", "import type { ServerResponse } from 'node:http'\n\nexport function writeJson(res: ServerResponse, status: number, body: unknown, headers?: Record<string, string>): void {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...headers })\n res.end(JSON.stringify(body))\n}\n", "/**\n * dsh-usage-dashboard host plugin.\n *\n * Registers the package's own fenced JSON API under\n * `/api/dsh-usage-dashboard` (balance + usage + session), consumed by the\n * client half through same-origin fetch. `balance` and `usage` are memoized\n * with a TTL so that tab switches and the widget's polling don't replay\n * session logs or hammer the DeepSeek API; responses carry `Cache-Control`\n * so the browser's HTTP cache serves repeat requests without a network\n * round trip. `?refresh=1` bypasses the memo for an explicit force refresh.\n * `session` (usage for exactly one session, keyed by `?id=`) reads a single\n * session log and is deliberately not memoized \u2014 it is cheap and must\n * reflect that session's live state, not a 5-minute-stale snapshot.\n */\nimport type { HostContext } from './context.ts'\nimport { isValidSessionId } from './contract.ts'\nimport { memoize } from './memo.ts'\nimport { isTrustedApiRequest } from './trust-fence.ts'\nimport { fetchBalance, fetchSessionUsage, fetchUsage } from './usage.ts'\nimport { writeJson } from './wire.ts'\n\n/** Plugin identity for the cordis.patch.yml row (and the client bundle id). */\nexport const name = 'dsh-usage-dashboard'\n\n/** Services required before load: the web server plus the two data sources. */\nexport const inject = ['webServer', 'credentials', 'sessionPersistence']\n\n/** Freshness windows: balance 60s (the widget also polls every 60s), usage\n * 5min (aggregating every session log is the expensive path). */\nconst BALANCE_TTL_MS = 60_000\nconst USAGE_TTL_MS = 5 * 60_000\n\n/**\n * Mount the /api/dsh-usage-dashboard routes.\n * @param ctx - host Cordis context.\n */\nexport function apply(ctx: HostContext): void {\n const balanceMemo = memoize(BALANCE_TTL_MS, () => fetchBalance(ctx.credentials))\n const usageMemo = memoize(USAGE_TTL_MS, () => fetchUsage(ctx.sessionPersistence))\n\n ctx.effect(() => ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dsh-usage-dashboard',\n handler: async (req, res) => {\n if (!isTrustedApiRequest(req)) {\n writeJson(res, 403, { ok: false, error: 'forbidden' })\n return\n }\n if (req.method !== 'GET') {\n writeJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n const url = new URL(req.url ?? '/', 'http://dsh.internal')\n const pathname = url.pathname\n const force = url.searchParams.get('refresh') === '1'\n if (pathname === '/api/dsh-usage-dashboard/balance') {\n const payload = await (force ? balanceMemo.refresh() : balanceMemo.get())\n const headers = payload.ok\n ? { 'cache-control': `private, max-age=${Math.round(BALANCE_TTL_MS / 1000)}` }\n : { 'cache-control': 'no-store' }\n writeJson(res, 200, payload, headers)\n } else if (pathname === '/api/dsh-usage-dashboard/usage') {\n const payload = await (force ? usageMemo.refresh() : usageMemo.get())\n const headers = payload.ok\n ? { 'cache-control': `private, max-age=${Math.round(USAGE_TTL_MS / 1000)}` }\n : { 'cache-control': 'no-store' }\n writeJson(res, 200, payload, headers)\n } else if (pathname === '/api/dsh-usage-dashboard/session') {\n const id = url.searchParams.get('id')\n // `id` arrives verbatim from the browser-controlled `?id=` query\n // parameter (unlike `usage`'s session ids, which the host itself\n // enumerates via `persistence.list()`), so a malformed value is\n // rejected here \u2014 before it ever reaches `persistence.readFrom` \u2014\n // rather than trusted to fetchSessionUsage's own defense-in-depth\n // check. Reject as early as the route layer allows.\n if (id === null || id.trim() === '' || !isValidSessionId(id)) {\n // A missing/blank/malformed `id` is the caller's mistake (bad\n // input), so it gets a real 4xx; any other failure (e.g. the log\n // itself unreadable) keeps the same \"200 with ok:false\" shape as\n // balance/usage so the client's one error-handling path covers all\n // three endpoints.\n const error = id === null || id.trim() === '' ? '\u7F3A\u5C11\u4F1A\u8BDD id' : '\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5'\n writeJson(res, 400, { ok: false, error }, { 'cache-control': 'no-store' })\n return\n }\n const payload = await fetchSessionUsage(ctx.sessionPersistence, id)\n // id is already known-well-formed here, so any remaining failure is\n // downstream (e.g. the log itself unreadable) and keeps the same\n // \"200 with ok:false\" shape as balance/usage.\n writeJson(res, 200, payload, { 'cache-control': 'no-store' })\n } else {\n writeJson(res, 404, { ok: false, error: 'not found' })\n }\n },\n }), 'dsh-usage-dashboard: /api routes')\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["daily", "hourly"]
7
7
  }
@@ -7,4 +7,6 @@ export declare const getCachedUsageAt: () => number | null;
7
7
  export declare const subscribeUsage: (fn: () => void) => (() => void);
8
8
  export declare function fetchBalance(force?: boolean): Promise<BalanceResponse>;
9
9
  export declare function fetchUsage(force?: boolean): Promise<UsageResponse>;
10
+ export declare const getCachedSessionUsage: (sessionId: string) => SessionUsageResponse | null;
11
+ export declare const subscribeSessionUsage: (sessionId: string, fn: () => void) => (() => void);
10
12
  export declare function fetchSessionUsage(sessionId: string, force?: boolean): Promise<SessionUsageResponse>;
@@ -1,6 +1,7 @@
1
1
  /** Structural faces the client half reads from the Cordis context. */
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
3
  import type { Translate } from './i18n.tsx';
4
+ import type { ConversationViewSlotEntry } from './views.ts';
4
5
  export interface SlotOptions {
5
6
  name: string;
6
7
  id?: string;
@@ -11,6 +12,8 @@ export interface SlotOptions {
11
12
  export interface SlotsFace {
12
13
  inject(key: string, callback: () => unknown): unknown;
13
14
  register(options: SlotOptions, component: unknown): unknown;
15
+ entries(name: string): ConversationViewSlotEntry[];
16
+ subscribe(name: string, fn: () => void): () => void;
14
17
  }
15
18
  export interface LocaleFace {
16
19
  register(namespace: string, dictionaries: {
@@ -8,8 +8,10 @@
8
8
  import { type ReactElement } from 'react';
9
9
  import { type Translate } from './i18n.tsx';
10
10
  import type { ModelUsage } from '../contract.ts';
11
+ import type { ConversationViewsSource } from './views.ts';
11
12
  export declare const modelKeyOf: (m: ModelUsage) => string;
12
13
  export declare const modelNameOf: (m: ModelUsage, t?: Translate) => string;
13
14
  export declare function BalanceDashboard(props: {
14
15
  sessionId?: string;
16
+ views: ConversationViewsSource;
15
17
  }): ReactElement;
@@ -21,3 +21,8 @@ export declare function getShellFrame(node: HTMLElement | null): HTMLElement | n
21
21
  * can call `.getBoundingClientRect()`; callers that want to observe size
22
22
  * changes (e.g. `ResizeObserver`) need the element itself. */
23
23
  export declare function getComposerElement(frame: HTMLElement | null): HTMLElement | null;
24
+ /** Resolve the active `conversation.view` id from the host's semantic tablist.
25
+ * The DOM intentionally carries no view id, but it renders buttons in the
26
+ * exact order of the slot ledger. Indexing into that same live ledger avoids
27
+ * translated-label matching and remains compatible with third-party tabs. */
28
+ export declare function getActiveConversationViewId(frame: HTMLElement | null, viewIds: string[]): string | null;
@@ -216,7 +216,10 @@ export declare const zh: {
216
216
  readonly 'links.status': "服务状态";
217
217
  readonly 'settings.title': "设置";
218
218
  readonly 'settings.widget': "在其他页面显示右下角悬浮额度窗口";
219
- readonly 'settings.widgetHint': "当前额度页已展示完整数据,悬浮窗会自动隐藏;切回 Chat 或 Trajectory 后恢复。";
219
+ readonly 'settings.widgetHint': "额度页会自动隐藏浮窗;其他页面是否展示由下方选择决定。";
220
+ readonly 'settings.widgetTabs': "展示浮窗的页面";
221
+ readonly 'settings.widgetTabsHint': "页面列表来自 DSH 的实时 Tab 注册表;其他插件新增的 Tab 也会自动出现在这里。";
222
+ readonly 'settings.widgetTabsEmpty': "当前没有可配置的会话页面。";
220
223
  readonly 'settings.lowBalance': "余额预警线";
221
224
  readonly 'settings.lowBalanceOn': "余额低于 {amount} {currency} 时,这里和悬浮窗都会转为警示色。填 0 关闭。";
222
225
  readonly 'settings.lowBalanceOff': "已关闭余额预警。填一个大于 0 的数开启。";
@@ -229,6 +232,8 @@ export declare const zh: {
229
232
  readonly 'widget.lowTitle': "余额低于预警线 {amount}";
230
233
  readonly 'widget.todayCostTitle': "今日消耗(估算)· {age}";
231
234
  readonly 'widget.todayCost': "今日消耗";
235
+ readonly 'widget.sessionCostTitle': "当前会话累计消耗(估算)";
236
+ readonly 'widget.sessionCost': "当前会话";
232
237
  readonly 'widget.noBalance': "暂无余额数据";
233
238
  readonly 'widget.dragCard': "拖动到四个角落";
234
239
  readonly 'widget.dragGrip': "拖动以移动";
@@ -237,6 +242,9 @@ export declare const zh: {
237
242
  readonly 'widget.collapse': "收起";
238
243
  readonly 'widget.expandAria': "展开额度窗口";
239
244
  readonly 'widget.collapseAria': "收起额度窗口";
245
+ readonly 'messageCost.label': "本轮费用";
246
+ readonly 'messageCost.tooltip': "由 dsh-usage-dashboard 提供 · 本轮对话费用为估算值";
247
+ readonly 'messageCost.aria': "{amount},本轮对话估算费用,由 dsh-usage-dashboard 提供";
240
248
  readonly 'boundary.title': "额度面板出错了";
241
249
  readonly 'boundary.body': "渲染时抛了异常,通常是浏览器里存着旧版本的缓存数据。清掉本插件的本地数据再重新加载就能恢复。";
242
250
  readonly 'boundary.reset': "清除本地数据并重新加载";
@@ -0,0 +1,10 @@
1
+ /** Estimated spend contributed beside each completed chat turn. */
2
+ import { type ReactElement } from 'react';
3
+ export interface MessageCostProps {
4
+ sessionId: string;
5
+ messageId: string;
6
+ }
7
+ /** Small costs need more than the dashboard's two-decimal money formatter or
8
+ * almost every individual turn would misleadingly read as ¥0.00. */
9
+ export declare const fmtTurnCost: (cost: number) => string;
10
+ export declare function MessageCost({ sessionId, messageId }: MessageCostProps): ReactElement | null;
@@ -5,6 +5,10 @@ export interface Store<T> {
5
5
  }
6
6
  /** Whether the floating balance widget is shown. */
7
7
  export declare const widgetVisibleStore: Store<boolean>;
8
+ /** Conversation view ids on which the widget is allowed to appear. `null`
9
+ * means "all available views" and preserves the pre-setting default; once a
10
+ * user customizes the list, newly installed plugin tabs arrive unchecked. */
11
+ export declare const widgetTabIdsStore: Store<string[] | null>;
8
12
  /** Balance (in the account's own currency) below which both surfaces warn.
9
13
  * 0 turns the warning off. */
10
14
  export declare const lowBalanceStore: Store<number>;