mirror of
https://github.com/Routstr/routstrd.git
synced 2026-08-11 20:47:58 +00:00
- Moved daemon communication helpers (loadConfig, callDaemon, isDaemonRunning, startDaemonProcess, ensureDaemonRunning, handleDaemonCommand) from cli-shared.ts to src/utils/daemon-client.ts - Moved the refund command from cli-shared.ts into cli.ts with the rest of the CLI commands - cli.ts now imports program directly from commander and daemon helpers from utils/daemon-client - TUI imports are updated to use utils/daemon-client - Removed src/cli-shared.ts
239 lines
7.8 KiB
TypeScript
239 lines
7.8 KiB
TypeScript
import type { UsageTrackingEntry } from "../../daemon/types.ts";
|
|
import { callDaemon, isDaemonRunning } from "../../utils/daemon-client.ts";
|
|
import type { ClientStats, DayStats, ModelStats, ProviderStats, UsageStats } from "./types.ts";
|
|
|
|
export interface BalanceKey {
|
|
id: string;
|
|
name: string;
|
|
balance: number;
|
|
}
|
|
|
|
export interface BalanceInfo {
|
|
keys: BalanceKey[];
|
|
total: number;
|
|
unit: "sat";
|
|
apikeysCalled: number;
|
|
}
|
|
|
|
export interface StatusInfo {
|
|
daemon: string;
|
|
wallet: string;
|
|
mode: "xcashu" | "apikeys";
|
|
error?: string;
|
|
}
|
|
|
|
export async function fetchStatus(): Promise<StatusInfo | null> {
|
|
try {
|
|
const running = await isDaemonRunning();
|
|
if (!running) return null;
|
|
|
|
const result = await callDaemon("/status");
|
|
if (result.error) return null;
|
|
|
|
const output = result.output as {
|
|
daemon?: string;
|
|
wallet?: string;
|
|
mode?: "xcashu" | "apikeys";
|
|
error?: string;
|
|
};
|
|
|
|
return {
|
|
daemon: output?.daemon || "unknown",
|
|
wallet: output?.wallet || "unknown",
|
|
mode: output?.mode || "apikeys",
|
|
error: output?.error,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function fetchBalance(): Promise<BalanceInfo | null> {
|
|
try {
|
|
const running = await isDaemonRunning();
|
|
if (!running) return null;
|
|
|
|
const result = await callDaemon("/keys/balance");
|
|
if (result.error) return null;
|
|
|
|
const output = result.output as {
|
|
keys?: BalanceKey[];
|
|
total?: number;
|
|
unit?: string;
|
|
apikeysCalled?: number;
|
|
};
|
|
|
|
return {
|
|
keys: output?.keys || [],
|
|
total: output?.total || 0,
|
|
unit: (output?.unit as "sat") || "sat",
|
|
apikeysCalled: output?.apikeysCalled || 0,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function fetchUsage(limit = 10000): Promise<UsageStats | null> {
|
|
try {
|
|
const running = await isDaemonRunning();
|
|
if (!running) return null;
|
|
|
|
const result = await callDaemon(`/usage?limit=${limit}`);
|
|
if (result.error) return null;
|
|
|
|
// The daemon returns { output: [...] } where output is the entries array directly
|
|
const entries = result.output as UsageTrackingEntry[] | undefined;
|
|
const entriesArray = Array.isArray(entries) ? entries : [];
|
|
|
|
// Calculate totals from entries
|
|
const totals = entriesArray.reduce(
|
|
(acc, entry) => ({
|
|
promptTokens: acc.promptTokens + entry.promptTokens,
|
|
completionTokens: acc.completionTokens + entry.completionTokens,
|
|
totalTokens: acc.totalTokens + entry.totalTokens,
|
|
satsCost: acc.satsCost + entry.satsCost,
|
|
}),
|
|
{ promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 },
|
|
);
|
|
|
|
return {
|
|
entries: entriesArray,
|
|
totalEntries: entriesArray.length,
|
|
totalSatsCost: totals.satsCost,
|
|
recentSatsCost: totals.satsCost, // For now, recent = total since we don't have time window
|
|
limit,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function getTodayStart(): number {
|
|
const now = new Date();
|
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
}
|
|
|
|
export function formatDate(timestamp: number): string {
|
|
return new Date(timestamp).toISOString().split("T")[0] ?? "";
|
|
}
|
|
|
|
export function formatTime(timestamp: number): string {
|
|
return new Date(timestamp).toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
}
|
|
|
|
export function formatNumber(n: number): string {
|
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
|
if (n >= 1_000) return (n / 1_000).toFixed(1) + "K";
|
|
return n.toString();
|
|
}
|
|
|
|
export function getDayStats(entries: UsageTrackingEntry[]): Map<string, DayStats> {
|
|
const days = new Map<string, DayStats>();
|
|
for (const entry of entries) {
|
|
const date = formatDate(entry.timestamp);
|
|
const existing = days.get(date) || { date, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
days.set(date, {
|
|
...existing,
|
|
requests: existing.requests + 1,
|
|
satsCost: existing.satsCost + entry.satsCost,
|
|
promptTokens: existing.promptTokens + entry.promptTokens,
|
|
completionTokens: existing.completionTokens + entry.completionTokens,
|
|
totalTokens: existing.totalTokens + entry.totalTokens,
|
|
});
|
|
}
|
|
return days;
|
|
}
|
|
|
|
export function getHourlyToday(entries: UsageTrackingEntry[]): Map<number, DayStats> {
|
|
const todayStart = getTodayStart();
|
|
const hours = new Map<number, DayStats>();
|
|
for (const entry of entries) {
|
|
if (entry.timestamp < todayStart) continue;
|
|
const hour = new Date(entry.timestamp).getHours();
|
|
const existing = hours.get(hour) || {
|
|
date: formatDate(entry.timestamp), requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0,
|
|
};
|
|
hours.set(hour, {
|
|
...existing,
|
|
requests: existing.requests + 1,
|
|
satsCost: existing.satsCost + entry.satsCost,
|
|
promptTokens: existing.promptTokens + entry.promptTokens,
|
|
completionTokens: existing.completionTokens + entry.completionTokens,
|
|
totalTokens: existing.totalTokens + entry.totalTokens,
|
|
});
|
|
}
|
|
return hours;
|
|
}
|
|
|
|
export function getModelStats(entries: UsageTrackingEntry[]): ModelStats[] {
|
|
const models = new Map<string, ModelStats>();
|
|
for (const entry of entries) {
|
|
const existing = models.get(entry.modelId) || {
|
|
modelId: entry.modelId, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0,
|
|
};
|
|
models.set(entry.modelId, {
|
|
...existing,
|
|
requests: existing.requests + 1,
|
|
satsCost: existing.satsCost + entry.satsCost,
|
|
promptTokens: existing.promptTokens + entry.promptTokens,
|
|
completionTokens: existing.completionTokens + entry.completionTokens,
|
|
totalTokens: existing.totalTokens + entry.totalTokens,
|
|
});
|
|
}
|
|
return Array.from(models.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
}
|
|
|
|
export function getProviderStats(entries: UsageTrackingEntry[]): ProviderStats[] {
|
|
const providers = new Map<string, ProviderStats>();
|
|
for (const entry of entries) {
|
|
const url = entry.baseUrl || "unknown";
|
|
const existing = providers.get(url) || {
|
|
baseUrl: url, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0,
|
|
};
|
|
providers.set(url, {
|
|
...existing,
|
|
requests: existing.requests + 1,
|
|
satsCost: existing.satsCost + entry.satsCost,
|
|
promptTokens: existing.promptTokens + entry.promptTokens,
|
|
completionTokens: existing.completionTokens + entry.completionTokens,
|
|
totalTokens: existing.totalTokens + entry.totalTokens,
|
|
});
|
|
}
|
|
return Array.from(providers.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
}
|
|
|
|
export function getClientStats(entries: UsageTrackingEntry[]): ClientStats[] {
|
|
const clients = new Map<string, ClientStats>();
|
|
for (const entry of entries) {
|
|
const client = entry.client || "unknown";
|
|
const existing = clients.get(client) || {
|
|
client, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0,
|
|
};
|
|
clients.set(client, {
|
|
...existing,
|
|
requests: existing.requests + 1,
|
|
satsCost: existing.satsCost + entry.satsCost,
|
|
promptTokens: existing.promptTokens + entry.promptTokens,
|
|
completionTokens: existing.completionTokens + entry.completionTokens,
|
|
totalTokens: existing.totalTokens + entry.totalTokens,
|
|
});
|
|
}
|
|
return Array.from(clients.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
}
|
|
|
|
export function getTotals(entries: UsageTrackingEntry[] | undefined) {
|
|
if (!entries || !Array.isArray(entries)) {
|
|
return { promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 };
|
|
}
|
|
return entries.reduce(
|
|
(acc, entry) => ({
|
|
promptTokens: acc.promptTokens + entry.promptTokens,
|
|
completionTokens: acc.completionTokens + entry.completionTokens,
|
|
totalTokens: acc.totalTokens + entry.totalTokens,
|
|
satsCost: acc.satsCost + entry.satsCost,
|
|
}),
|
|
{ promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 }
|
|
);
|
|
}
|