From 61acc0b5fb996ce5ced2b7ff2a1d7216f9971404 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:17:13 +0800 Subject: [PATCH] added npub based filter! --- src/daemon/http/index.ts | 8 ++++- src/daemon/http/usage-summary.ts | 54 +++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 7132315..89d453c 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -1139,8 +1139,14 @@ export function createDaemonRequestHandler(deps: { if (req.method === "GET" && url.pathname === "/usage/summary") { try { const tz = Number.parseInt(url.searchParams.get("tz") || "0", 10) || 0; + const npubFilter = url.searchParams.get("npub")?.trim(); const clients = getClientsFromStore(deps.store); - const summary = await getUsageSummary(deps.usageTrackingDriver, clients, tz); + const clientFilter = npubFilter + ? clients + .filter((c) => c.ownerNpub === npubFilter) + .map((c) => c.clientId) + : undefined; + const summary = await getUsageSummary(deps.usageTrackingDriver, clients, tz, clientFilter); sendJson(res, 200, { output: summary }); } catch (error) { sendJson(res, 500, { error: toErrorMessage(error) }); diff --git a/src/daemon/http/usage-summary.ts b/src/daemon/http/usage-summary.ts index ddf7b5e..e909fa2 100644 --- a/src/daemon/http/usage-summary.ts +++ b/src/daemon/http/usage-summary.ts @@ -160,13 +160,14 @@ export async function getUsageSummary( driver: UsageTrackingDriver, clients: ClientEntry[], tzOffsetMinutes: number, + /** If set, only include usage for these client IDs (e.g. from `?npub=` filtering). */ + clientFilter?: string[], ): Promise { - // Cache key: total row count + per-client identity (id + ownerNpub) + tz. - // Including ownerNpub ensures a client assignment change invalidates the cache - // even though the row count doesn't change. - const count = await driver.count(); + // Cache key: total row count + per-client identity + filter + tz. + const count = await driver.count(clientFilter ? { clients: clientFilter } : {}); const clientIdentity = clients.map((c) => `${c.clientId}:${c.ownerNpub ?? ""}`).join(","); - const cacheKey = `${count}:${clientIdentity}:${tzOffsetMinutes}`; + const filterKey = clientFilter ? `:f:${clientFilter.sort().join(",")}` : ""; + const cacheKey = `${count}:${clientIdentity}:${tzOffsetMinutes}${filterKey}`; const now = Date.now(); if ( @@ -177,29 +178,57 @@ export async function getUsageSummary( return _cache.summary; } + // Base filter applied to every aggregate/list/count call + const baseFilter = clientFilter ? { clients: clientFilter } as const : {}; + + // Short-circuit: if the filter yields no rows, return zeroed summary + if (count === 0) { + const zeroStat: StatRow = { + requests: 0, promptTokens: 0, completionTokens: 0, + totalTokens: 0, cost: 0, satsCost: 0, + }; + const zeroSummary: UsageSummary = { + generatedAt: now, + totals: zeroStat, + models: [], + providers: [], + clients: [], + npubs: [], + days: [], + hoursToday: [], + sizeBuckets: { + tiny: emptyBucket(), small: emptyBucket(), medium: emptyBucket(), + large: emptyBucket(), huge: emptyBucket(), + }, + recent: [], + }; + _cache = { key: cacheKey, summary: zeroSummary }; + return zeroSummary; + } + // ── Totals ───────────────────────────────────────────────────────────────── - const [totalsRow] = await driver.aggregate({}); + const [totalsRow] = await driver.aggregate({ ...baseFilter }); const totals: StatRow = totalsRow ? rowToStat(totalsRow) : { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0, satsCost: 0, }; // ── Models ───────────────────────────────────────────────────────────────── - const modelRows = await driver.aggregate({ groupBy: "modelId" }); + const modelRows = await driver.aggregate({ ...baseFilter, groupBy: "modelId" }); const models: ModelSummary[] = modelRows.map((r) => ({ modelId: r.group ?? "unknown", ...rowToStat(r), })); // ── Providers ────────────────────────────────────────────────────────────── - const providerRows = await driver.aggregate({ groupBy: "baseUrl" }); + const providerRows = await driver.aggregate({ ...baseFilter, groupBy: "baseUrl" }); const providers: ProviderSummary[] = providerRows.map((r) => ({ baseUrl: r.group ?? "unknown", ...rowToStat(r), })); // ── Clients ──────────────────────────────────────────────────────────────── - const clientRows = await driver.aggregate({ groupBy: "client" }); + const clientRows = await driver.aggregate({ ...baseFilter, groupBy: "client" }); const clientSummaries: ClientSummary[] = clientRows.map((r) => ({ client: r.group ?? "unknown", ...rowToStat(r), @@ -213,6 +242,7 @@ export async function getUsageSummary( for (let i = 0; i < topClientRows.length; i++) { const clientId = topClientRows[i]!.group!; const topModelRows = await driver.aggregate({ + ...baseFilter, groupBy: "modelId", client: clientId, }); @@ -281,6 +311,7 @@ export async function getUsageSummary( // ── Days (last 30, most-recent-first) ───────────────────────────────────── const dayRows = await driver.aggregate({ + ...baseFilter, groupBy: "day", tzOffsetMinutes, after: now - 30 * 86400000, @@ -292,6 +323,7 @@ export async function getUsageSummary( // ── Hours today ──────────────────────────────────────────────────────────── const todayStartUtc = startOfLocalDayUtc(now, tzOffsetMinutes); const hourRows = await driver.aggregate({ + ...baseFilter, groupBy: "hour", tzOffsetMinutes, after: todayStartUtc - 1, @@ -305,11 +337,11 @@ export async function getUsageSummary( // The SDK's aggregate() no longer supports token-range filters // (minTotalTokens/maxTotalTokens were removed in SDK pr-8), so bucket in // JS from a single list() call instead of five aggregate() queries. - const allEntries = await driver.list(); + const allEntries = await driver.list(baseFilter); const sizeBuckets = computeSizeBuckets(allEntries); // ── Recent entries ───────────────────────────────────────────────────────── - const recent = await driver.list({ limit: 50 }); + const recent = await driver.list({ ...baseFilter, limit: 50 }); const summary: UsageSummary = { generatedAt: now,