diff --git a/src/cli.ts b/src/cli.ts index 978f492..6f2be68 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -26,6 +26,19 @@ type RoutstrModel = { context_length?: number; }; +type UsageEntry = { + id: string; + timestamp: number; + modelId: string; + baseUrl: string; + requestId: string; + cost: number; + satsCost: number; + promptTokens: number; + completionTokens: number; + totalTokens: number; +}; + const cliVersion = "0.1.0"; async function initDaemon(): Promise { @@ -286,6 +299,61 @@ program } }); +program + .command("usage") + .description("Show recent usage logs and total sats cost") + .option("-n, --limit ", "Number of recent usage entries", "10") + .action(async (options: { limit: string }) => { + await ensureDaemonRunning(); + + const requested = Number.parseInt(options.limit, 10); + const limit = + Number.isFinite(requested) && requested > 0 ? Math.min(requested, 1000) : 10; + + const result = await callDaemon(`/usage?limit=${limit}`); + if (result.error) { + console.log(result.error); + process.exit(1); + } + + const output = result.output as + | { + entries?: UsageEntry[]; + totalEntries?: number; + totalSatsCost?: number; + recentSatsCost?: number; + limit?: number; + } + | undefined; + + const entries = output?.entries || []; + const totalEntries = output?.totalEntries || 0; + const totalSatsCost = output?.totalSatsCost || 0; + const recentSatsCost = output?.recentSatsCost || 0; + + console.log(`Usage entries: showing ${entries.length} of ${totalEntries}`); + console.log(`Total sats cost (all time): ${totalSatsCost.toFixed(3)} sats`); + console.log(`Sats cost (shown entries): ${recentSatsCost.toFixed(3)} sats`); + + if (entries.length === 0) { + console.log("No usage entries yet."); + return; + } + + console.log(""); + entries.forEach((entry, index) => { + const time = new Date(entry.timestamp).toISOString(); + const provider = entry.baseUrl || "unknown"; + const reqId = entry.requestId || "unknown"; + console.log( + `${index + 1}. ${time} | ${entry.modelId} | ${provider} | ${entry.satsCost.toFixed(3)} sats`, + ); + console.log( + ` tokens p/c/t: ${entry.promptTokens}/${entry.completionTokens}/${entry.totalTokens} | request: ${reqId}`, + ); + }); + }); + // Stop program .command("stop") diff --git a/src/daemon.ts b/src/daemon.ts index babf1d0..eb1d8dd 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -816,6 +816,49 @@ async function main(): Promise { return; } + if (req.method === "GET" && url.pathname === "/usage") { + try { + const requestedLimit = Number.parseInt( + url.searchParams.get("limit") || "10", + 10, + ); + const limit = + Number.isFinite(requestedLimit) && requestedLimit > 0 + ? Math.min(requestedLimit, 1000) + : 10; + + const usageTracking = + ((store.getState().usageTracking || []) as UsageTrackingEntry[]) || + []; + const recent = usageTracking.slice(-limit).reverse(); + const totalSatsCost = usageTracking.reduce( + (sum, entry) => sum + (entry.satsCost || 0), + 0, + ); + const recentSatsCost = recent.reduce( + (sum, entry) => sum + (entry.satsCost || 0), + 0, + ); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + output: { + entries: recent, + totalEntries: usageTracking.length, + totalSatsCost, + recentSatsCost, + limit, + }, + }), + ); + } catch (error) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: String(error) })); + } + return; + } + if (req.method !== "POST") { res.writeHead(405, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Only POST is supported." }));