routstrd 0.2.16 → 0.2.18
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/dist/daemon/index.js +49 -8
- package/dist/index.js +194 -56
- package/package.json +2 -2
- package/src/daemon/models.ts +18 -2
- package/src/start-daemon.ts +1 -16
- package/src/tui/usage/app.ts +20 -10
- package/src/tui/usage/constants.ts +15 -2
- package/src/tui/usage/data.ts +75 -1
- package/src/tui/usage/render.ts +111 -10
- package/src/tui/usage/types.ts +10 -1
package/dist/daemon/index.js
CHANGED
|
@@ -37553,6 +37553,7 @@ var createBunSqliteUsageTrackingDriver2 = (options = {}) => {
|
|
|
37553
37553
|
|
|
37554
37554
|
// src/daemon/wallet/index.ts
|
|
37555
37555
|
init_cashu_ts_es();
|
|
37556
|
+
init_dist3();
|
|
37556
37557
|
|
|
37557
37558
|
// src/daemon/wallet/cocod-client.ts
|
|
37558
37559
|
import { createHash } from "crypto";
|
|
@@ -37908,6 +37909,9 @@ async function createWalletAdapter(options = {}) {
|
|
|
37908
37909
|
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
37909
37910
|
continue;
|
|
37910
37911
|
}
|
|
37912
|
+
if (errorMessage.includes("Not enough proofs")) {
|
|
37913
|
+
throw new InsufficientBalanceError(amount, 0);
|
|
37914
|
+
}
|
|
37911
37915
|
logger3.error("Error in walletAdapter sendToken:", error);
|
|
37912
37916
|
throw error;
|
|
37913
37917
|
}
|
|
@@ -37940,7 +37944,7 @@ async function createWalletAdapter(options = {}) {
|
|
|
37940
37944
|
}
|
|
37941
37945
|
|
|
37942
37946
|
// src/daemon/models.ts
|
|
37943
|
-
function createModelService(modelManager) {
|
|
37947
|
+
function createModelService(modelManager, store) {
|
|
37944
37948
|
let providerBootstrapPromise = null;
|
|
37945
37949
|
const ensureProvidersBootstrapped = () => {
|
|
37946
37950
|
if (!providerBootstrapPromise) {
|
|
@@ -37949,6 +37953,16 @@ function createModelService(modelManager) {
|
|
|
37949
37953
|
const providers = await modelManager.bootstrapProviders(false);
|
|
37950
37954
|
logger3.log(`Bootstrapped ${providers.length} providers`);
|
|
37951
37955
|
await modelManager.fetchModels(providers);
|
|
37956
|
+
const { baseUrlsList, setBaseUrlsList } = store.getState();
|
|
37957
|
+
const existing = new Set(baseUrlsList);
|
|
37958
|
+
const merged = [
|
|
37959
|
+
...baseUrlsList,
|
|
37960
|
+
...providers.filter((url2) => !existing.has(url2))
|
|
37961
|
+
];
|
|
37962
|
+
if (merged.length !== baseUrlsList.length) {
|
|
37963
|
+
setBaseUrlsList(merged);
|
|
37964
|
+
logger3.log(`Synced ${merged.length - baseUrlsList.length} new provider(s) into store`);
|
|
37965
|
+
}
|
|
37952
37966
|
logger3.log("Provider bootstrap complete.");
|
|
37953
37967
|
})().catch((error) => {
|
|
37954
37968
|
logger3.error("Provider bootstrap failed:", error);
|
|
@@ -42656,6 +42670,18 @@ async function buildWalletDetails(deps) {
|
|
|
42656
42670
|
activeMint: deps.walletAdapter.getActiveMintUrl()
|
|
42657
42671
|
};
|
|
42658
42672
|
}
|
|
42673
|
+
function makeSdkLogger(prefix) {
|
|
42674
|
+
const tag = prefix ? `[${prefix}]` : undefined;
|
|
42675
|
+
const fmt = (...args) => tag ? [tag, ...args] : args;
|
|
42676
|
+
return {
|
|
42677
|
+
log: (...args) => logger3.log(...fmt(...args)),
|
|
42678
|
+
warn: (...args) => logger3.log(...fmt(...args)),
|
|
42679
|
+
error: (...args) => logger3.error(...fmt(...args)),
|
|
42680
|
+
debug: (...args) => logger3.debug(...fmt(...args)),
|
|
42681
|
+
child: (p) => makeSdkLogger(prefix ? `${prefix}:${p}` : p)
|
|
42682
|
+
};
|
|
42683
|
+
}
|
|
42684
|
+
var sdkLogger = makeSdkLogger();
|
|
42659
42685
|
function createDaemonRequestHandler(deps) {
|
|
42660
42686
|
return async function handler(req, res) {
|
|
42661
42687
|
const host = req.headers.host || "localhost";
|
|
@@ -43228,7 +43254,9 @@ function createDaemonRequestHandler(deps) {
|
|
|
43228
43254
|
}
|
|
43229
43255
|
try {
|
|
43230
43256
|
await deps.ensureProvidersBootstrapped();
|
|
43231
|
-
|
|
43257
|
+
const reqId = randomBytes6(4).toString("hex");
|
|
43258
|
+
const reqLogger = sdkLogger.child(`req:${reqId}`);
|
|
43259
|
+
logger3.log(`[req:${reqId}] Routing request with path: `, url2.pathname);
|
|
43232
43260
|
const response = await routeRequests({
|
|
43233
43261
|
modelId,
|
|
43234
43262
|
requestBody,
|
|
@@ -43244,7 +43272,8 @@ function createDaemonRequestHandler(deps) {
|
|
|
43244
43272
|
mode: deps.mode,
|
|
43245
43273
|
usageTrackingDriver: deps.usageTrackingDriver,
|
|
43246
43274
|
sdkStore: deps.store,
|
|
43247
|
-
providerManager: deps.providerManager
|
|
43275
|
+
providerManager: deps.providerManager,
|
|
43276
|
+
logger: reqLogger
|
|
43248
43277
|
});
|
|
43249
43278
|
res.statusCode = response.status;
|
|
43250
43279
|
response.headers.forEach((value, key) => {
|
|
@@ -43327,6 +43356,18 @@ async function refreshModelsAndIntegrations(getRoutstr21Models, config, label =
|
|
|
43327
43356
|
|
|
43328
43357
|
// src/daemon/index.ts
|
|
43329
43358
|
init_dist3();
|
|
43359
|
+
function makeSdkLogger2(prefix) {
|
|
43360
|
+
const tag = prefix ? `[${prefix}]` : undefined;
|
|
43361
|
+
const fmt = (...args) => tag ? [tag, ...args] : args;
|
|
43362
|
+
return {
|
|
43363
|
+
log: (...args) => logger3.log(...fmt(...args)),
|
|
43364
|
+
warn: (...args) => logger3.log(...fmt(...args)),
|
|
43365
|
+
error: (...args) => logger3.error(...fmt(...args)),
|
|
43366
|
+
debug: (...args) => logger3.debug(...fmt(...args)),
|
|
43367
|
+
child: (p) => makeSdkLogger2(prefix ? `${prefix}:${p}` : p)
|
|
43368
|
+
};
|
|
43369
|
+
}
|
|
43370
|
+
var daemonSdkLogger = makeSdkLogger2();
|
|
43330
43371
|
async function main() {
|
|
43331
43372
|
const args = parseArgs(process.argv);
|
|
43332
43373
|
const config = await loadDaemonConfig();
|
|
@@ -43335,7 +43376,7 @@ async function main() {
|
|
|
43335
43376
|
await ensureDirs();
|
|
43336
43377
|
const updatedConfig = { ...config, port, provider };
|
|
43337
43378
|
saveDaemonConfig(updatedConfig);
|
|
43338
|
-
const sqliteDriver = await createBunSqliteDriver2(DB_PATH);
|
|
43379
|
+
const sqliteDriver = await createBunSqliteDriver2(DB_PATH, { logger: daemonSdkLogger });
|
|
43339
43380
|
const { store, hydrate } = createSdkStore({ driver: sqliteDriver });
|
|
43340
43381
|
await hydrate;
|
|
43341
43382
|
const { Database } = await import("bun:sqlite");
|
|
@@ -43345,11 +43386,11 @@ async function main() {
|
|
|
43345
43386
|
legacyStorageDriver: sqliteDriver
|
|
43346
43387
|
});
|
|
43347
43388
|
const discoveryAdapter = createDiscoveryAdapterFromStore(store);
|
|
43348
|
-
const providerRegistry = createProviderRegistryFromStore(store);
|
|
43389
|
+
const providerRegistry = createProviderRegistryFromStore(store, daemonSdkLogger);
|
|
43349
43390
|
const storageAdapter = createStorageAdapterFromStore(store);
|
|
43350
|
-
const modelManager = new ModelManager(discoveryAdapter);
|
|
43351
|
-
const providerManager = new ProviderManager(providerRegistry, store);
|
|
43352
|
-
const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders } = createModelService(modelManager);
|
|
43391
|
+
const modelManager = new ModelManager(discoveryAdapter, { logger: daemonSdkLogger });
|
|
43392
|
+
const providerManager = new ProviderManager(providerRegistry, store, daemonSdkLogger);
|
|
43393
|
+
const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders } = createModelService(modelManager, store);
|
|
43353
43394
|
const walletClient = createCocodClient({ cocodPath: config.cocodPath });
|
|
43354
43395
|
const walletAdapter = await createWalletAdapter({
|
|
43355
43396
|
cocodPath: config.cocodPath,
|
package/dist/index.js
CHANGED
|
@@ -2302,14 +2302,6 @@ var init_process_lock = () => {
|
|
|
2302
2302
|
};
|
|
2303
2303
|
|
|
2304
2304
|
// src/start-daemon.ts
|
|
2305
|
-
import { existsSync as existsSync2 } from "fs";
|
|
2306
|
-
function getTodayLogFile() {
|
|
2307
|
-
const now = new Date;
|
|
2308
|
-
const year = now.getFullYear();
|
|
2309
|
-
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
2310
|
-
const day = String(now.getDate()).padStart(2, "0");
|
|
2311
|
-
return `${LOGS_DIR2}/${year}-${month}-${day}.log`;
|
|
2312
|
-
}
|
|
2313
2305
|
async function isDaemonHealthy(port) {
|
|
2314
2306
|
const controller = new AbortController;
|
|
2315
2307
|
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
@@ -2339,12 +2331,8 @@ async function startDaemonUnlocked(options) {
|
|
|
2339
2331
|
if (options.provider) {
|
|
2340
2332
|
args.push("--provider", options.provider);
|
|
2341
2333
|
}
|
|
2342
|
-
if (!existsSync2(LOGS_DIR2)) {
|
|
2343
|
-
await Bun.$`mkdir -p ${LOGS_DIR2}`;
|
|
2344
|
-
}
|
|
2345
2334
|
const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname;
|
|
2346
|
-
const
|
|
2347
|
-
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")} >> "${todayLogFile}" 2>&1`;
|
|
2335
|
+
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`;
|
|
2348
2336
|
const proc = Bun.spawn(["sh", "-c", shellCmd], {
|
|
2349
2337
|
stdout: "inherit",
|
|
2350
2338
|
stderr: "inherit",
|
|
@@ -9380,10 +9368,10 @@ var init_nip98 = __esm(() => {
|
|
|
9380
9368
|
});
|
|
9381
9369
|
|
|
9382
9370
|
// src/utils/daemon-client.ts
|
|
9383
|
-
import { existsSync as
|
|
9371
|
+
import { existsSync as existsSync2 } from "fs";
|
|
9384
9372
|
async function loadConfig() {
|
|
9385
9373
|
try {
|
|
9386
|
-
if (
|
|
9374
|
+
if (existsSync2(CONFIG_FILE)) {
|
|
9387
9375
|
const content = await Bun.file(CONFIG_FILE).text();
|
|
9388
9376
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
9389
9377
|
}
|
|
@@ -13681,17 +13669,23 @@ var require_browser = __commonJS((exports) => {
|
|
|
13681
13669
|
});
|
|
13682
13670
|
|
|
13683
13671
|
// src/tui/usage/constants.ts
|
|
13684
|
-
|
|
13672
|
+
function getVisibleTabs(hasNpubs) {
|
|
13673
|
+
const tabs = ALL_TABS.filter((t) => t.id !== "npubs" || hasNpubs);
|
|
13674
|
+
return tabs.map((t, i2) => ({ ...t, key: String(i2 + 1) }));
|
|
13675
|
+
}
|
|
13676
|
+
var ALL_TABS, TABS, COLORS, MODEL_COLORS, CLIENT_COLORS;
|
|
13685
13677
|
var init_constants = __esm(() => {
|
|
13686
|
-
|
|
13678
|
+
ALL_TABS = [
|
|
13687
13679
|
{ id: "overview", name: "Overview", key: "1" },
|
|
13688
13680
|
{ id: "today", name: "Today", key: "2" },
|
|
13689
13681
|
{ id: "models", name: "Models", key: "3" },
|
|
13690
13682
|
{ id: "providers", name: "Providers", key: "4" },
|
|
13691
13683
|
{ id: "tokens", name: "Tokens", key: "5" },
|
|
13692
13684
|
{ id: "clients", name: "Clients", key: "6" },
|
|
13693
|
-
{ id: "
|
|
13685
|
+
{ id: "npubs", name: "Npubs", key: "7" },
|
|
13686
|
+
{ id: "recent", name: "Recent", key: "8" }
|
|
13694
13687
|
];
|
|
13688
|
+
TABS = getVisibleTabs(false);
|
|
13695
13689
|
COLORS = {
|
|
13696
13690
|
reset: "\x1B[0m",
|
|
13697
13691
|
bold: "\x1B[1m",
|
|
@@ -13915,6 +13909,58 @@ function getClientStats(entries) {
|
|
|
13915
13909
|
}
|
|
13916
13910
|
return Array.from(clients.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
13917
13911
|
}
|
|
13912
|
+
async function fetchClients() {
|
|
13913
|
+
try {
|
|
13914
|
+
const running = await isDaemonRunning();
|
|
13915
|
+
if (!running)
|
|
13916
|
+
return [];
|
|
13917
|
+
const result = await callDaemon("/clients");
|
|
13918
|
+
if (result.error)
|
|
13919
|
+
return [];
|
|
13920
|
+
const output = result.output;
|
|
13921
|
+
return (output?.clients || []).map((c) => ({
|
|
13922
|
+
clientId: c.id,
|
|
13923
|
+
name: c.name,
|
|
13924
|
+
ownerNpub: c.ownerNpub
|
|
13925
|
+
}));
|
|
13926
|
+
} catch {
|
|
13927
|
+
return [];
|
|
13928
|
+
}
|
|
13929
|
+
}
|
|
13930
|
+
function hasAnyNpubs(clients) {
|
|
13931
|
+
return clients.some((c) => !!c.ownerNpub);
|
|
13932
|
+
}
|
|
13933
|
+
function getNpubStats(entries, clients) {
|
|
13934
|
+
const clientNpubMap = new Map;
|
|
13935
|
+
for (const c of clients) {
|
|
13936
|
+
if (c.ownerNpub) {
|
|
13937
|
+
clientNpubMap.set(c.clientId, c.ownerNpub);
|
|
13938
|
+
}
|
|
13939
|
+
}
|
|
13940
|
+
const npubs = new Map;
|
|
13941
|
+
for (const entry of entries) {
|
|
13942
|
+
const npub = clientNpubMap.get(entry.client || "");
|
|
13943
|
+
if (!npub)
|
|
13944
|
+
continue;
|
|
13945
|
+
const existing = npubs.get(npub) || {
|
|
13946
|
+
npub,
|
|
13947
|
+
requests: 0,
|
|
13948
|
+
satsCost: 0,
|
|
13949
|
+
promptTokens: 0,
|
|
13950
|
+
completionTokens: 0,
|
|
13951
|
+
totalTokens: 0
|
|
13952
|
+
};
|
|
13953
|
+
npubs.set(npub, {
|
|
13954
|
+
...existing,
|
|
13955
|
+
requests: existing.requests + 1,
|
|
13956
|
+
satsCost: existing.satsCost + entry.satsCost,
|
|
13957
|
+
promptTokens: existing.promptTokens + entry.promptTokens,
|
|
13958
|
+
completionTokens: existing.completionTokens + entry.completionTokens,
|
|
13959
|
+
totalTokens: existing.totalTokens + entry.totalTokens
|
|
13960
|
+
});
|
|
13961
|
+
}
|
|
13962
|
+
return Array.from(npubs.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
13963
|
+
}
|
|
13918
13964
|
function getTotals(entries) {
|
|
13919
13965
|
if (!entries || !Array.isArray(entries)) {
|
|
13920
13966
|
return { promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 };
|
|
@@ -14061,10 +14107,11 @@ function formatReqs(value) {
|
|
|
14061
14107
|
return (value / 1000).toFixed(1) + "k";
|
|
14062
14108
|
return value.toString();
|
|
14063
14109
|
}
|
|
14064
|
-
function renderHeader(activeTab, width) {
|
|
14110
|
+
function renderHeader(activeTab, width, visibleTabs) {
|
|
14065
14111
|
const title = `${COLORS.bold}${COLORS.cyan}ROUTSTRD USAGE MONITOR${COLORS.reset}`;
|
|
14066
14112
|
const vimIndicator = `${COLORS.yellow}[vim]${COLORS.reset}`;
|
|
14067
|
-
const
|
|
14113
|
+
const maxKey = visibleTabs.length;
|
|
14114
|
+
const help = `${COLORS.dim}[Q] Quit [\u2191\u2193] Scroll [\u2190\u2192] Tabs [1-${maxKey}] Tabs [R] Refresh${COLORS.reset}`;
|
|
14068
14115
|
const fill = width - title.length - help.length - vimIndicator.length - 6;
|
|
14069
14116
|
return `${title}${vimIndicator}${" ".repeat(Math.max(1, fill))}${help}
|
|
14070
14117
|
`;
|
|
@@ -14080,8 +14127,8 @@ function renderSearchBar() {
|
|
|
14080
14127
|
${searchLine}${placeholder}
|
|
14081
14128
|
`;
|
|
14082
14129
|
}
|
|
14083
|
-
function renderTabs(activeTab) {
|
|
14084
|
-
const tabStr =
|
|
14130
|
+
function renderTabs(activeTab, visibleTabs) {
|
|
14131
|
+
const tabStr = visibleTabs.map((tab) => tab.id === activeTab ? `${COLORS.bgBlue} ${tab.key}:${tab.name} ${COLORS.reset}` : `${COLORS.dim}[${tab.key}]${COLORS.reset} ${tab.name}`).join(" ");
|
|
14085
14132
|
return `${" ".repeat(2)}${tabStr}
|
|
14086
14133
|
`;
|
|
14087
14134
|
}
|
|
@@ -14452,25 +14499,106 @@ function renderClients(stats, width) {
|
|
|
14452
14499
|
}
|
|
14453
14500
|
return output;
|
|
14454
14501
|
}
|
|
14502
|
+
function renderNpubs(stats, clients, width) {
|
|
14503
|
+
const npubStats = getNpubStats(stats.entries, clients);
|
|
14504
|
+
if (npubStats.length === 0)
|
|
14505
|
+
return renderBox(["No npub data available"], width, "Npub Breakdown");
|
|
14506
|
+
const totalCost = stats.totalSatsCost;
|
|
14507
|
+
const maxCost = npubStats[0].satsCost;
|
|
14508
|
+
const lines = [];
|
|
14509
|
+
const col1 = 24;
|
|
14510
|
+
const col2 = 12;
|
|
14511
|
+
const col3 = 24;
|
|
14512
|
+
const col4 = 12;
|
|
14513
|
+
const hNpub = "Npub".padEnd(col1);
|
|
14514
|
+
const hReqs = "Requests".padEnd(col2);
|
|
14515
|
+
const hCost = "Cost".padEnd(col3);
|
|
14516
|
+
const hTok = "Tokens".padEnd(col4);
|
|
14517
|
+
lines.push(`${COLORS.bold}${hNpub}${hReqs}${hCost}${hTok}Avg Cost${COLORS.reset}`);
|
|
14518
|
+
lines.push(COLORS.dim + "\u2500".repeat(Math.max(0, width - 4)) + COLORS.reset);
|
|
14519
|
+
startBarSection("npub-detail", col1);
|
|
14520
|
+
for (const npub of npubStats) {
|
|
14521
|
+
const pct = totalCost > 0 ? (npub.satsCost / totalCost * 100).toFixed(1) : "0.0";
|
|
14522
|
+
const avgCostFormatted = formatCost(npub.requests > 0 ? npub.satsCost / npub.requests : 0);
|
|
14523
|
+
const shortNpub = truncateNpub(npub.npub);
|
|
14524
|
+
const dNpub = shortNpub.padEnd(col1);
|
|
14525
|
+
const dReqs = formatReqs(npub.requests).padEnd(col2);
|
|
14526
|
+
const dCost = `${formatCost(npub.satsCost)} sats (${pct}%)`.padEnd(col3);
|
|
14527
|
+
const dTok = formatNumber(npub.totalTokens).padEnd(col4);
|
|
14528
|
+
const dAvg = `${avgCostFormatted} sats/req`;
|
|
14529
|
+
lines.push(`${COLORS.magenta}${COLORS.bold}${dNpub}${COLORS.reset}` + `${dReqs}` + `${COLORS.green}${dCost}${COLORS.reset}` + `${COLORS.dim}${dTok}${dAvg}${COLORS.reset}`);
|
|
14530
|
+
lines.push(` ${COLORS.dim}${npub.npub}${COLORS.reset}`);
|
|
14531
|
+
lines.push(` ${renderBarChart("", npub.satsCost, maxCost, width - 6, COLORS.magenta, Number(pct), "npub-detail")}`);
|
|
14532
|
+
lines.push("");
|
|
14533
|
+
}
|
|
14534
|
+
endBarSection("npub-detail");
|
|
14535
|
+
let output = renderBox(lines, width, "Npub Breakdown");
|
|
14536
|
+
const npubModelMap = new Map;
|
|
14537
|
+
const clientNpubMap = new Map;
|
|
14538
|
+
for (const c of clients) {
|
|
14539
|
+
if (c.ownerNpub)
|
|
14540
|
+
clientNpubMap.set(c.clientId, c.ownerNpub);
|
|
14541
|
+
}
|
|
14542
|
+
for (const entry of stats.entries) {
|
|
14543
|
+
const npub = clientNpubMap.get(entry.client || "");
|
|
14544
|
+
if (!npub)
|
|
14545
|
+
continue;
|
|
14546
|
+
const model = entry.modelId;
|
|
14547
|
+
if (!npubModelMap.has(npub))
|
|
14548
|
+
npubModelMap.set(npub, new Map);
|
|
14549
|
+
const modelMap = npubModelMap.get(npub);
|
|
14550
|
+
const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 };
|
|
14551
|
+
modelMap.set(model, {
|
|
14552
|
+
requests: existing.requests + 1,
|
|
14553
|
+
satsCost: existing.satsCost + entry.satsCost,
|
|
14554
|
+
tokens: existing.tokens + entry.totalTokens
|
|
14555
|
+
});
|
|
14556
|
+
}
|
|
14557
|
+
const npubModelLines = [];
|
|
14558
|
+
for (const topNpub of npubStats.slice(0, 5)) {
|
|
14559
|
+
const modelMap = npubModelMap.get(topNpub.npub);
|
|
14560
|
+
if (!modelMap)
|
|
14561
|
+
continue;
|
|
14562
|
+
const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5);
|
|
14563
|
+
npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`);
|
|
14564
|
+
for (const [model, data] of models) {
|
|
14565
|
+
npubModelLines.push(` ${MODEL_COLORS[model] || MODEL_COLORS.default}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`);
|
|
14566
|
+
}
|
|
14567
|
+
npubModelLines.push("");
|
|
14568
|
+
}
|
|
14569
|
+
if (npubModelLines.length > 0) {
|
|
14570
|
+
output += `
|
|
14571
|
+
` + renderBox(npubModelLines, width, "Top Models per Npub");
|
|
14572
|
+
}
|
|
14573
|
+
return output;
|
|
14574
|
+
}
|
|
14575
|
+
function truncateNpub(npub) {
|
|
14576
|
+
if (npub.length <= 24)
|
|
14577
|
+
return npub;
|
|
14578
|
+
return npub.slice(0, 10) + "\u2026" + npub.slice(-6);
|
|
14579
|
+
}
|
|
14455
14580
|
function renderRecent(stats, width) {
|
|
14456
14581
|
const recentEntries = stats.entries.slice(0, 50);
|
|
14457
14582
|
if (recentEntries.length === 0)
|
|
14458
14583
|
return renderBox(["No recent entries"], width, "Recent Requests");
|
|
14584
|
+
const clientCol = 14;
|
|
14459
14585
|
const lines = [];
|
|
14460
|
-
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"TOKENS".padEnd(10)} ${"COST".padEnd(12)} ${"PROVIDER".
|
|
14586
|
+
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"TOKENS".padEnd(10)} ${"COST".padEnd(12)} ${"PROVIDER".padEnd(16)} ${"CLIENT".slice(0, clientCol)}${COLORS.reset}`);
|
|
14461
14587
|
lines.push(COLORS.dim + "\u2500".repeat(width - 4) + COLORS.reset);
|
|
14462
14588
|
for (const entry of recentEntries) {
|
|
14463
14589
|
const time = formatTime(entry.timestamp).slice(0, 8);
|
|
14464
14590
|
const model = entry.modelId.slice(0, 18).padEnd(18);
|
|
14465
14591
|
const tokens = `${formatNumber(entry.totalTokens).padEnd(6)} (${formatNumber(entry.promptTokens)}+${formatNumber(entry.completionTokens)})`;
|
|
14466
14592
|
const cost = `${formatCost(entry.satsCost).padEnd(8)} sats`;
|
|
14467
|
-
const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0,
|
|
14468
|
-
const
|
|
14469
|
-
|
|
14593
|
+
const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0, 16).padEnd(16);
|
|
14594
|
+
const clientName = (entry.client || "unknown").slice(0, clientCol - 1);
|
|
14595
|
+
const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white;
|
|
14596
|
+
const modelColor = MODEL_COLORS[entry.modelId] || MODEL_COLORS.default;
|
|
14597
|
+
lines.push(`${COLORS.dim}${time}${COLORS.reset} ${modelColor}${model}${COLORS.reset} ${tokens.padEnd(10)} ${COLORS.green}${cost}${COLORS.reset} ${COLORS.dim}${provider}${COLORS.reset} ${clientColor}${clientName}${COLORS.reset}`);
|
|
14470
14598
|
}
|
|
14471
14599
|
return renderBox(lines, width, `Recent Requests (${stats.entries.length} shown)`);
|
|
14472
14600
|
}
|
|
14473
|
-
function renderTabContent(activeTab, stats, balance, status, width) {
|
|
14601
|
+
function renderTabContent(activeTab, stats, balance, status, width, clients = []) {
|
|
14474
14602
|
switch (activeTab) {
|
|
14475
14603
|
case "overview":
|
|
14476
14604
|
return renderOverview(stats, balance, status, width);
|
|
@@ -14484,6 +14612,8 @@ function renderTabContent(activeTab, stats, balance, status, width) {
|
|
|
14484
14612
|
return renderTokens(stats, width);
|
|
14485
14613
|
case "clients":
|
|
14486
14614
|
return renderClients(stats, width);
|
|
14615
|
+
case "npubs":
|
|
14616
|
+
return renderNpubs(stats, clients, width);
|
|
14487
14617
|
case "recent":
|
|
14488
14618
|
return renderRecent(stats, width);
|
|
14489
14619
|
default:
|
|
@@ -14513,6 +14643,8 @@ async function runUsageTui() {
|
|
|
14513
14643
|
let stats = null;
|
|
14514
14644
|
let balance = null;
|
|
14515
14645
|
let status = null;
|
|
14646
|
+
let clients = [];
|
|
14647
|
+
let visibleTabs = getVisibleTabs(false);
|
|
14516
14648
|
let refreshInterval = null;
|
|
14517
14649
|
let shouldUpdate = true;
|
|
14518
14650
|
let autoRefresh = true;
|
|
@@ -14552,6 +14684,13 @@ async function runUsageTui() {
|
|
|
14552
14684
|
stats = await fetchUsage(1e4);
|
|
14553
14685
|
balance = await fetchBalance();
|
|
14554
14686
|
status = await fetchStatus();
|
|
14687
|
+
clients = await fetchClients();
|
|
14688
|
+
const npubsVisible = hasAnyNpubs(clients);
|
|
14689
|
+
visibleTabs = getVisibleTabs(npubsVisible);
|
|
14690
|
+
if (currentTab === "npubs" && !npubsVisible) {
|
|
14691
|
+
currentTab = "clients";
|
|
14692
|
+
vimState.scrollPos = 0;
|
|
14693
|
+
}
|
|
14555
14694
|
shouldUpdate = false;
|
|
14556
14695
|
}
|
|
14557
14696
|
if (!stats) {
|
|
@@ -14561,9 +14700,9 @@ async function runUsageTui() {
|
|
|
14561
14700
|
Press Q to quit.`);
|
|
14562
14701
|
return;
|
|
14563
14702
|
}
|
|
14564
|
-
const content = renderTabContent(currentTab, stats, balance, status, width);
|
|
14703
|
+
const content = renderTabContent(currentTab, stats, balance, status, width, clients);
|
|
14565
14704
|
const footer = `${COLORS.dim}Press [Q] to quit, [R] to refresh, [A] to toggle auto-refresh${autoRefresh ? " (on)" : " (off)"} scroll:${vimState.scrollPos}${COLORS.reset}${vimState.mode === "normal" ? ` ${COLORS.yellow}vim: hjkl/arrows, / search, g top, gg bottom${COLORS.reset}` : ""}`;
|
|
14566
|
-
const chrome = renderHeader(currentTab, width) + renderTabs(currentTab) + renderSeparator(width) + renderSearchBar();
|
|
14705
|
+
const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
|
|
14567
14706
|
const chromeLines = chrome.split(`
|
|
14568
14707
|
`).length - 1;
|
|
14569
14708
|
const footerSeparator = renderSeparator(width);
|
|
@@ -14637,15 +14776,15 @@ Press Q to quit.`);
|
|
|
14637
14776
|
return;
|
|
14638
14777
|
}
|
|
14639
14778
|
if (key === "l" || key === "\x1B[C" || key === "\x1BOC") {
|
|
14640
|
-
const currentIdx =
|
|
14641
|
-
currentTab =
|
|
14779
|
+
const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
|
|
14780
|
+
currentTab = visibleTabs[(currentIdx + 1) % visibleTabs.length].id;
|
|
14642
14781
|
vimState.scrollPos = 0;
|
|
14643
14782
|
render2(false);
|
|
14644
14783
|
return;
|
|
14645
14784
|
}
|
|
14646
14785
|
if (key === "h" || key === "\x1B[D" || key === "\x1BOD") {
|
|
14647
|
-
const currentIdx =
|
|
14648
|
-
currentTab =
|
|
14786
|
+
const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
|
|
14787
|
+
currentTab = visibleTabs[(currentIdx - 1 + visibleTabs.length) % visibleTabs.length].id;
|
|
14649
14788
|
vimState.scrollPos = 0;
|
|
14650
14789
|
render2(false);
|
|
14651
14790
|
return;
|
|
@@ -14724,7 +14863,7 @@ Press Q to quit.`);
|
|
|
14724
14863
|
render2(false);
|
|
14725
14864
|
return;
|
|
14726
14865
|
}
|
|
14727
|
-
const tab =
|
|
14866
|
+
const tab = visibleTabs.find((t) => t.key === key);
|
|
14728
14867
|
if (tab) {
|
|
14729
14868
|
currentTab = tab.id;
|
|
14730
14869
|
vimState.scrollPos = 0;
|
|
@@ -14747,7 +14886,6 @@ var init_app = __esm(() => {
|
|
|
14747
14886
|
init_state();
|
|
14748
14887
|
init_constants();
|
|
14749
14888
|
init_render();
|
|
14750
|
-
init_daemon_client();
|
|
14751
14889
|
});
|
|
14752
14890
|
|
|
14753
14891
|
// src/tui/usage/index.ts
|
|
@@ -14789,7 +14927,7 @@ import { join as join3 } from "path";
|
|
|
14789
14927
|
// src/integrations/opencode.ts
|
|
14790
14928
|
init_logger();
|
|
14791
14929
|
init_daemon_client();
|
|
14792
|
-
import { existsSync as
|
|
14930
|
+
import { existsSync as existsSync3, mkdirSync } from "fs";
|
|
14793
14931
|
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
14794
14932
|
import { dirname as dirname2 } from "path";
|
|
14795
14933
|
var OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
|
|
@@ -14801,7 +14939,7 @@ Installing routstr models in opencode.json...`);
|
|
|
14801
14939
|
const baseUrl = getDaemonBaseUrl(config);
|
|
14802
14940
|
let opencodeConfig;
|
|
14803
14941
|
try {
|
|
14804
|
-
if (
|
|
14942
|
+
if (existsSync3(configPath)) {
|
|
14805
14943
|
const content = await readFile2(configPath, "utf-8");
|
|
14806
14944
|
opencodeConfig = JSON.parse(content);
|
|
14807
14945
|
} else {
|
|
@@ -14846,7 +14984,7 @@ Installing routstr models in opencode.json...`);
|
|
|
14846
14984
|
// src/integrations/pi.ts
|
|
14847
14985
|
init_logger();
|
|
14848
14986
|
init_daemon_client();
|
|
14849
|
-
import { existsSync as
|
|
14987
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
14850
14988
|
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
14851
14989
|
import { dirname as dirname3 } from "path";
|
|
14852
14990
|
async function installPiIntegration(config, apiKey, integrationConfig) {
|
|
@@ -14857,7 +14995,7 @@ Installing routstr models in pi models.json...`);
|
|
|
14857
14995
|
const baseUrl = `${getDaemonBaseUrl(config)}/v1`;
|
|
14858
14996
|
let piConfig = {};
|
|
14859
14997
|
try {
|
|
14860
|
-
if (
|
|
14998
|
+
if (existsSync4(configPath)) {
|
|
14861
14999
|
const content = await readFile3(configPath, "utf-8");
|
|
14862
15000
|
piConfig = JSON.parse(content);
|
|
14863
15001
|
}
|
|
@@ -14894,7 +15032,7 @@ Installing routstr models in pi models.json...`);
|
|
|
14894
15032
|
// src/integrations/openclaw.ts
|
|
14895
15033
|
init_logger();
|
|
14896
15034
|
init_daemon_client();
|
|
14897
|
-
import { existsSync as
|
|
15035
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
14898
15036
|
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
14899
15037
|
import { dirname as dirname4 } from "path";
|
|
14900
15038
|
var OPENCLAW_PROVIDER_ID = "routstr";
|
|
@@ -14908,7 +15046,7 @@ Installing routstr models in openclaw.json...`);
|
|
|
14908
15046
|
const baseUrl = getDaemonBaseUrl(config);
|
|
14909
15047
|
let openclawConfig = {};
|
|
14910
15048
|
try {
|
|
14911
|
-
if (
|
|
15049
|
+
if (existsSync5(configPath)) {
|
|
14912
15050
|
const content = await readFile4(configPath, "utf-8");
|
|
14913
15051
|
openclawConfig = JSON.parse(content);
|
|
14914
15052
|
}
|
|
@@ -14970,7 +15108,7 @@ Installing routstr models in openclaw.json...`);
|
|
|
14970
15108
|
// src/integrations/claudecode.ts
|
|
14971
15109
|
init_logger();
|
|
14972
15110
|
init_daemon_client();
|
|
14973
|
-
import { existsSync as
|
|
15111
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
14974
15112
|
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
14975
15113
|
import { dirname as dirname5 } from "path";
|
|
14976
15114
|
async function installClaudeCodeIntegration(config, apiKey, integrationConfig) {
|
|
@@ -14981,7 +15119,7 @@ Installing routstr configuration in ${configPath}...`);
|
|
|
14981
15119
|
const baseUrl = getDaemonBaseUrl(config);
|
|
14982
15120
|
let settings = {};
|
|
14983
15121
|
try {
|
|
14984
|
-
if (
|
|
15122
|
+
if (existsSync6(configPath)) {
|
|
14985
15123
|
const content = await readFile5(configPath, "utf-8");
|
|
14986
15124
|
settings = JSON.parse(content);
|
|
14987
15125
|
}
|
|
@@ -15028,7 +15166,7 @@ Installing routstr configuration in ${configPath}...`);
|
|
|
15028
15166
|
// src/integrations/hermes.ts
|
|
15029
15167
|
init_logger();
|
|
15030
15168
|
init_daemon_client();
|
|
15031
|
-
import { existsSync as
|
|
15169
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
15032
15170
|
import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
15033
15171
|
import { dirname as dirname6 } from "path";
|
|
15034
15172
|
async function installHermesIntegration(config, apiKey, integrationConfig) {
|
|
@@ -15057,7 +15195,7 @@ Installing routstr configuration in ${configPath}...`);
|
|
|
15057
15195
|
}
|
|
15058
15196
|
let content = "";
|
|
15059
15197
|
try {
|
|
15060
|
-
if (
|
|
15198
|
+
if (existsSync7(configPath)) {
|
|
15061
15199
|
content = await readFile6(configPath, "utf-8");
|
|
15062
15200
|
}
|
|
15063
15201
|
} catch (error) {
|
|
@@ -15309,7 +15447,7 @@ async function addClientAction(options) {
|
|
|
15309
15447
|
// src/cli.ts
|
|
15310
15448
|
init_config();
|
|
15311
15449
|
init_logger();
|
|
15312
|
-
import { existsSync as
|
|
15450
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
|
|
15313
15451
|
import { execSync } from "child_process";
|
|
15314
15452
|
|
|
15315
15453
|
// src/integrations/index.ts
|
|
@@ -15463,7 +15601,7 @@ init_nip98();
|
|
|
15463
15601
|
init_esm();
|
|
15464
15602
|
|
|
15465
15603
|
// src/daemon/wallet/cocod-client.ts
|
|
15466
|
-
import { existsSync as
|
|
15604
|
+
import { existsSync as existsSync8 } from "fs";
|
|
15467
15605
|
init_logger();
|
|
15468
15606
|
init_process_lock();
|
|
15469
15607
|
var DEFAULT_CONFIG_DIR = process.env.COCOD_DIR || `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`;
|
|
@@ -15475,7 +15613,7 @@ function resolveCocodExecutable(cocodPath) {
|
|
|
15475
15613
|
async function isCocodInstalled(cocodPath) {
|
|
15476
15614
|
const executable = resolveCocodExecutable(cocodPath);
|
|
15477
15615
|
if (executable.includes("/")) {
|
|
15478
|
-
return
|
|
15616
|
+
return existsSync8(executable);
|
|
15479
15617
|
}
|
|
15480
15618
|
try {
|
|
15481
15619
|
const proc = Bun.spawn({
|
|
@@ -15491,7 +15629,7 @@ async function isCocodInstalled(cocodPath) {
|
|
|
15491
15629
|
// package.json
|
|
15492
15630
|
var package_default = {
|
|
15493
15631
|
name: "routstrd",
|
|
15494
|
-
version: "0.2.
|
|
15632
|
+
version: "0.2.18",
|
|
15495
15633
|
module: "src/index.ts",
|
|
15496
15634
|
type: "module",
|
|
15497
15635
|
private: false,
|
|
@@ -15515,7 +15653,7 @@ var package_default = {
|
|
|
15515
15653
|
},
|
|
15516
15654
|
dependencies: {
|
|
15517
15655
|
"@cashu/cashu-ts": "^3.1.1",
|
|
15518
|
-
"@routstr/sdk": "^0.3.
|
|
15656
|
+
"@routstr/sdk": "^0.3.4",
|
|
15519
15657
|
"applesauce-core": "^5.1.0",
|
|
15520
15658
|
"applesauce-relay": "^5.1.0",
|
|
15521
15659
|
commander: "^14.0.2",
|
|
@@ -15570,11 +15708,11 @@ async function requireLocalDaemon() {
|
|
|
15570
15708
|
}
|
|
15571
15709
|
async function initDaemon() {
|
|
15572
15710
|
logger.log("Initializing routstrd...");
|
|
15573
|
-
if (!
|
|
15711
|
+
if (!existsSync9(CONFIG_DIR)) {
|
|
15574
15712
|
mkdirSync6(CONFIG_DIR, { recursive: true });
|
|
15575
15713
|
logger.log(`Created config directory: ${CONFIG_DIR}`);
|
|
15576
15714
|
}
|
|
15577
|
-
if (!
|
|
15715
|
+
if (!existsSync9(CONFIG_FILE)) {
|
|
15578
15716
|
const config2 = {
|
|
15579
15717
|
...DEFAULT_CONFIG,
|
|
15580
15718
|
cocodPath: null
|
|
@@ -15698,7 +15836,7 @@ program.command("remote <url>").description("Configure a remote daemon URL").act
|
|
|
15698
15836
|
console.error(`Invalid URL: ${url}`);
|
|
15699
15837
|
process.exit(1);
|
|
15700
15838
|
}
|
|
15701
|
-
if (!
|
|
15839
|
+
if (!existsSync9(CONFIG_DIR)) {
|
|
15702
15840
|
mkdirSync6(CONFIG_DIR, { recursive: true });
|
|
15703
15841
|
}
|
|
15704
15842
|
const config = await loadConfig();
|
|
@@ -16293,7 +16431,7 @@ serviceCmd.command("install").description("Install and start routstrd using PM2
|
|
|
16293
16431
|
const path = import.meta.require("path");
|
|
16294
16432
|
daemonPath = path.join(path.dirname(import.meta.url).replace("file://", ""), "daemon", "index.js");
|
|
16295
16433
|
}
|
|
16296
|
-
if (!
|
|
16434
|
+
if (!existsSync9(daemonPath)) {
|
|
16297
16435
|
console.error(`Could not find daemon at ${daemonPath}. Did you run 'bun run build'?`);
|
|
16298
16436
|
process.exit(1);
|
|
16299
16437
|
}
|
|
@@ -16431,14 +16569,14 @@ program.command("logs").description("View daemon logs").option("-f, --follow", "
|
|
|
16431
16569
|
const yesterday = new Date;
|
|
16432
16570
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
16433
16571
|
const yesterdayFile = getLogFileForDate2(yesterday);
|
|
16434
|
-
if (!
|
|
16572
|
+
if (!existsSync9(todayFile) && !existsSync9(yesterdayFile)) {
|
|
16435
16573
|
console.log("No log files found. Daemon may not have started yet.");
|
|
16436
16574
|
console.log(`Logs directory: ${LOGS_DIR2}`);
|
|
16437
16575
|
process.exit(1);
|
|
16438
16576
|
}
|
|
16439
16577
|
const lines = parseInt(options.lines, 10);
|
|
16440
16578
|
const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => {
|
|
16441
|
-
return
|
|
16579
|
+
return existsSync9(file) && files.indexOf(file) === index;
|
|
16442
16580
|
});
|
|
16443
16581
|
if (options.follow) {
|
|
16444
16582
|
const proc2 = Bun.spawn(["tail", "-n", String(lines), "-f", todayFile], {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@cashu/cashu-ts": "^3.1.1",
|
|
27
|
-
"@routstr/sdk": "^0.3.
|
|
27
|
+
"@routstr/sdk": "^0.3.4",
|
|
28
28
|
"applesauce-core": "^5.1.0",
|
|
29
29
|
"applesauce-relay": "^5.1.0",
|
|
30
30
|
"commander": "^14.0.2",
|
package/src/daemon/models.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ModelManager } from "@routstr/sdk";
|
|
1
|
+
import { ModelManager, type SdkStore } from "@routstr/sdk";
|
|
2
2
|
import type { ExposedModel } from "./types";
|
|
3
3
|
import { logger } from "../utils/logger";
|
|
4
4
|
|
|
@@ -16,7 +16,7 @@ export type ModelWithProviders = ExposedModel & {
|
|
|
16
16
|
providers: ModelProviderInfo[];
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
export function createModelService(modelManager: ModelManager) {
|
|
19
|
+
export function createModelService(modelManager: ModelManager, store: SdkStore) {
|
|
20
20
|
let providerBootstrapPromise: Promise<void> | null = null;
|
|
21
21
|
|
|
22
22
|
const ensureProvidersBootstrapped = (): Promise<void> => {
|
|
@@ -26,6 +26,22 @@ export function createModelService(modelManager: ModelManager) {
|
|
|
26
26
|
const providers = await modelManager.bootstrapProviders(false);
|
|
27
27
|
logger.log(`Bootstrapped ${providers.length} providers`);
|
|
28
28
|
await modelManager.fetchModels(providers);
|
|
29
|
+
|
|
30
|
+
// Sync discovered providers into the store so `providers list` reflects
|
|
31
|
+
// the same set that the model manager knows about.
|
|
32
|
+
const { baseUrlsList, setBaseUrlsList } = store.getState();
|
|
33
|
+
const existing = new Set(baseUrlsList);
|
|
34
|
+
const merged = [
|
|
35
|
+
...baseUrlsList,
|
|
36
|
+
...providers.filter((url) => !existing.has(url)),
|
|
37
|
+
];
|
|
38
|
+
if (merged.length !== baseUrlsList.length) {
|
|
39
|
+
setBaseUrlsList(merged);
|
|
40
|
+
logger.log(
|
|
41
|
+
`Synced ${merged.length - baseUrlsList.length} new provider(s) into store`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
logger.log("Provider bootstrap complete.");
|
|
30
46
|
})().catch((error) => {
|
|
31
47
|
logger.error("Provider bootstrap failed:", error);
|
package/src/start-daemon.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { logger } from "./utils/logger";
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
2
|
import { CONFIG_DIR, LOGS_DIR } from "./utils/config";
|
|
4
3
|
import { withCrossProcessLock } from "./utils/process-lock";
|
|
5
4
|
|
|
6
5
|
const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`;
|
|
7
6
|
|
|
8
|
-
function getTodayLogFile(): string {
|
|
9
|
-
const now = new Date();
|
|
10
|
-
const year = now.getFullYear();
|
|
11
|
-
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
12
|
-
const day = String(now.getDate()).padStart(2, "0");
|
|
13
|
-
return `${LOGS_DIR}/${year}-${month}-${day}.log`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
7
|
async function isDaemonHealthy(port: string): Promise<boolean> {
|
|
17
8
|
const controller = new AbortController();
|
|
18
9
|
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
@@ -48,14 +39,8 @@ async function startDaemonUnlocked(
|
|
|
48
39
|
args.push("--provider", options.provider);
|
|
49
40
|
}
|
|
50
41
|
|
|
51
|
-
// Ensure logs directory exists (logger handles date-based files)
|
|
52
|
-
if (!existsSync(LOGS_DIR)) {
|
|
53
|
-
await Bun.$`mkdir -p ${LOGS_DIR}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
42
|
const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname;
|
|
57
|
-
const
|
|
58
|
-
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")} >> "${todayLogFile}" 2>&1`;
|
|
43
|
+
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`;
|
|
59
44
|
|
|
60
45
|
const proc = Bun.spawn(["sh", "-c", shellCmd], {
|
|
61
46
|
stdout: "inherit",
|
package/src/tui/usage/app.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { getVisibleTabs } from "./constants.ts";
|
|
2
|
+
import type { Tab } from "./types.ts";
|
|
3
|
+
import { fetchBalance, fetchClients, fetchStatus, fetchUsage, hasAnyNpubs, isDaemonRunning, type BalanceInfo, type ClientInfo, type StatusInfo } from "./data.ts";
|
|
3
4
|
import {
|
|
4
5
|
applyScrollToContent,
|
|
5
6
|
exitSearchMode,
|
|
@@ -28,7 +29,6 @@ import {
|
|
|
28
29
|
import { COLORS } from "./constants.ts";
|
|
29
30
|
import { renderHeader, renderSearchBar, renderSeparator, renderTabContent, renderTabs } from "./render.ts";
|
|
30
31
|
import type { TabId, UsageStats } from "./types.ts";
|
|
31
|
-
import { isDaemonRunning } from "../../utils/daemon-client.ts";
|
|
32
32
|
|
|
33
33
|
export async function runUsageTui(): Promise<void> {
|
|
34
34
|
const running = await isDaemonRunning();
|
|
@@ -46,6 +46,8 @@ export async function runUsageTui(): Promise<void> {
|
|
|
46
46
|
let stats: UsageStats | null = null;
|
|
47
47
|
let balance: BalanceInfo | null = null;
|
|
48
48
|
let status: StatusInfo | null = null;
|
|
49
|
+
let clients: ClientInfo[] = [];
|
|
50
|
+
let visibleTabs: Tab[] = getVisibleTabs(false);
|
|
49
51
|
let refreshInterval: ReturnType<typeof setInterval> | null = null;
|
|
50
52
|
let shouldUpdate = true;
|
|
51
53
|
let autoRefresh = true;
|
|
@@ -90,6 +92,14 @@ export async function runUsageTui(): Promise<void> {
|
|
|
90
92
|
stats = await fetchUsage(10000);
|
|
91
93
|
balance = await fetchBalance();
|
|
92
94
|
status = await fetchStatus();
|
|
95
|
+
clients = await fetchClients();
|
|
96
|
+
const npubsVisible = hasAnyNpubs(clients);
|
|
97
|
+
visibleTabs = getVisibleTabs(npubsVisible);
|
|
98
|
+
// If current tab is npubs but it's no longer visible, fall back to clients
|
|
99
|
+
if (currentTab === "npubs" && !npubsVisible) {
|
|
100
|
+
currentTab = "clients";
|
|
101
|
+
vimState.scrollPos = 0;
|
|
102
|
+
}
|
|
93
103
|
shouldUpdate = false;
|
|
94
104
|
}
|
|
95
105
|
|
|
@@ -104,9 +114,9 @@ export async function runUsageTui(): Promise<void> {
|
|
|
104
114
|
return;
|
|
105
115
|
}
|
|
106
116
|
|
|
107
|
-
const content = renderTabContent(currentTab, stats, balance, status, width);
|
|
117
|
+
const content = renderTabContent(currentTab, stats, balance, status, width, clients);
|
|
108
118
|
const footer = `${COLORS.dim}Press [Q] to quit, [R] to refresh, [A] to toggle auto-refresh${autoRefresh ? " (on)" : " (off)"} scroll:${vimState.scrollPos}${COLORS.reset}${vimState.mode === "normal" ? ` ${COLORS.yellow}vim: hjkl/arrows, / search, g top, gg bottom${COLORS.reset}` : ""}`;
|
|
109
|
-
const chrome = renderHeader(currentTab, width) + renderTabs(currentTab) + renderSeparator(width) + renderSearchBar();
|
|
119
|
+
const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
|
|
110
120
|
const chromeLines = chrome.split("\n").length - 1;
|
|
111
121
|
const footerSeparator = renderSeparator(width);
|
|
112
122
|
const footerLines = footerSeparator.split("\n").length - 1;
|
|
@@ -176,15 +186,15 @@ export async function runUsageTui(): Promise<void> {
|
|
|
176
186
|
return;
|
|
177
187
|
}
|
|
178
188
|
if (key === "l" || key === "\x1b[C" || key === "\x1bOC") {
|
|
179
|
-
const currentIdx =
|
|
180
|
-
currentTab =
|
|
189
|
+
const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
|
|
190
|
+
currentTab = visibleTabs[(currentIdx + 1) % visibleTabs.length]!.id;
|
|
181
191
|
vimState.scrollPos = 0;
|
|
182
192
|
void render(false);
|
|
183
193
|
return;
|
|
184
194
|
}
|
|
185
195
|
if (key === "h" || key === "\x1b[D" || key === "\x1bOD") {
|
|
186
|
-
const currentIdx =
|
|
187
|
-
currentTab =
|
|
196
|
+
const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
|
|
197
|
+
currentTab = visibleTabs[(currentIdx - 1 + visibleTabs.length) % visibleTabs.length]!.id;
|
|
188
198
|
vimState.scrollPos = 0;
|
|
189
199
|
void render(false);
|
|
190
200
|
return;
|
|
@@ -226,7 +236,7 @@ export async function runUsageTui(): Promise<void> {
|
|
|
226
236
|
}
|
|
227
237
|
if (key === "\x1b") { scrollToTop(); void render(false); return; }
|
|
228
238
|
|
|
229
|
-
const tab =
|
|
239
|
+
const tab = visibleTabs.find((t) => t.key === key);
|
|
230
240
|
if (tab) {
|
|
231
241
|
currentTab = tab.id;
|
|
232
242
|
vimState.scrollPos = 0;
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import type { Tab } from "./types.ts";
|
|
2
2
|
|
|
3
|
-
export const
|
|
3
|
+
export const ALL_TABS: Tab[] = [
|
|
4
4
|
{ id: "overview", name: "Overview", key: "1" },
|
|
5
5
|
{ id: "today", name: "Today", key: "2" },
|
|
6
6
|
{ id: "models", name: "Models", key: "3" },
|
|
7
7
|
{ id: "providers", name: "Providers", key: "4" },
|
|
8
8
|
{ id: "tokens", name: "Tokens", key: "5" },
|
|
9
9
|
{ id: "clients", name: "Clients", key: "6" },
|
|
10
|
-
{ id: "
|
|
10
|
+
{ id: "npubs", name: "Npubs", key: "7" },
|
|
11
|
+
{ id: "recent", name: "Recent", key: "8" },
|
|
11
12
|
];
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Returns the visible tab list, hiding the Npubs tab when no clients
|
|
16
|
+
* have an ownerNpub. Keys are re-assigned sequentially (1..N).
|
|
17
|
+
*/
|
|
18
|
+
export function getVisibleTabs(hasNpubs: boolean): Tab[] {
|
|
19
|
+
const tabs = ALL_TABS.filter((t) => t.id !== "npubs" || hasNpubs);
|
|
20
|
+
return tabs.map((t, i) => ({ ...t, key: String(i + 1) }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Default tab list (no npub data yet). */
|
|
24
|
+
export const TABS: Tab[] = getVisibleTabs(false);
|
|
25
|
+
|
|
13
26
|
export const COLORS = {
|
|
14
27
|
reset: "\x1b[0m",
|
|
15
28
|
bold: "\x1b[1m",
|
package/src/tui/usage/data.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { UsageTrackingEntry } from "../../daemon/types.ts";
|
|
2
2
|
import { callDaemon, isDaemonRunning } from "../../utils/daemon-client.ts";
|
|
3
|
-
import type { ClientStats, DayStats, ModelStats, ProviderStats, UsageStats } from "./types.ts";
|
|
3
|
+
import type { ClientStats, DayStats, ModelStats, NpubStats, ProviderStats, UsageStats } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export { isDaemonRunning };
|
|
4
6
|
|
|
5
7
|
export interface BalanceKey {
|
|
6
8
|
id: string;
|
|
@@ -223,6 +225,78 @@ export function getClientStats(entries: UsageTrackingEntry[]): ClientStats[] {
|
|
|
223
225
|
return Array.from(clients.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
224
226
|
}
|
|
225
227
|
|
|
228
|
+
// ─── Client / Npub helpers ────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
export interface ClientInfo {
|
|
231
|
+
clientId: string;
|
|
232
|
+
name: string;
|
|
233
|
+
ownerNpub?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function fetchClients(): Promise<ClientInfo[]> {
|
|
237
|
+
try {
|
|
238
|
+
const running = await isDaemonRunning();
|
|
239
|
+
if (!running) return [];
|
|
240
|
+
|
|
241
|
+
const result = await callDaemon("/clients");
|
|
242
|
+
if (result.error) return [];
|
|
243
|
+
|
|
244
|
+
const output = result.output as {
|
|
245
|
+
clients?: Array<{ id: string; name: string; ownerNpub?: string }>;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return (output?.clients || []).map((c) => ({
|
|
249
|
+
clientId: c.id,
|
|
250
|
+
name: c.name,
|
|
251
|
+
ownerNpub: c.ownerNpub,
|
|
252
|
+
}));
|
|
253
|
+
} catch {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function hasAnyNpubs(clients: ClientInfo[]): boolean {
|
|
259
|
+
return clients.some((c) => !!c.ownerNpub);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function getNpubStats(entries: UsageTrackingEntry[], clients: ClientInfo[]): NpubStats[] {
|
|
263
|
+
// Build client-id → ownerNpub lookup
|
|
264
|
+
const clientNpubMap = new Map<string, string>();
|
|
265
|
+
for (const c of clients) {
|
|
266
|
+
if (c.ownerNpub) {
|
|
267
|
+
clientNpubMap.set(c.clientId, c.ownerNpub);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Aggregate usage per npub
|
|
272
|
+
const npubs = new Map<string, NpubStats>();
|
|
273
|
+
for (const entry of entries) {
|
|
274
|
+
const npub = clientNpubMap.get(entry.client || "");
|
|
275
|
+
if (!npub) continue; // skip entries whose client has no ownerNpub
|
|
276
|
+
|
|
277
|
+
const existing = npubs.get(npub) || {
|
|
278
|
+
npub,
|
|
279
|
+
requests: 0,
|
|
280
|
+
satsCost: 0,
|
|
281
|
+
promptTokens: 0,
|
|
282
|
+
completionTokens: 0,
|
|
283
|
+
totalTokens: 0,
|
|
284
|
+
};
|
|
285
|
+
npubs.set(npub, {
|
|
286
|
+
...existing,
|
|
287
|
+
requests: existing.requests + 1,
|
|
288
|
+
satsCost: existing.satsCost + entry.satsCost,
|
|
289
|
+
promptTokens: existing.promptTokens + entry.promptTokens,
|
|
290
|
+
completionTokens: existing.completionTokens + entry.completionTokens,
|
|
291
|
+
totalTokens: existing.totalTokens + entry.totalTokens,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return Array.from(npubs.values()).sort((a, b) => b.satsCost - a.satsCost);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── Totals ───────────────────────────────────────────────────────────
|
|
299
|
+
|
|
226
300
|
export function getTotals(entries: UsageTrackingEntry[] | undefined) {
|
|
227
301
|
if (!entries || !Array.isArray(entries)) {
|
|
228
302
|
return { promptTokens: 0, completionTokens: 0, totalTokens: 0, satsCost: 0 };
|
package/src/tui/usage/render.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CLIENT_COLORS, COLORS, MODEL_COLORS
|
|
1
|
+
import { CLIENT_COLORS, COLORS, MODEL_COLORS } from "./constants.ts";
|
|
2
|
+
import type { Tab } from "./types.ts";
|
|
2
3
|
import {
|
|
3
4
|
formatDate,
|
|
4
5
|
formatNumber,
|
|
@@ -7,9 +8,11 @@ import {
|
|
|
7
8
|
getDayStats,
|
|
8
9
|
getHourlyToday,
|
|
9
10
|
getModelStats,
|
|
11
|
+
getNpubStats,
|
|
10
12
|
getProviderStats,
|
|
11
13
|
getTodayStart,
|
|
12
14
|
getTotals,
|
|
15
|
+
type ClientInfo,
|
|
13
16
|
} from "./data.ts";
|
|
14
17
|
import { vimState } from "./state.ts";
|
|
15
18
|
import { stripAnsi } from "./terminal.ts";
|
|
@@ -30,10 +33,11 @@ function formatReqs(value: number): string {
|
|
|
30
33
|
return value.toString();
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
export function renderHeader(activeTab: TabId, width: number): string {
|
|
36
|
+
export function renderHeader(activeTab: TabId, width: number, visibleTabs: Tab[]): string {
|
|
34
37
|
const title = `${COLORS.bold}${COLORS.cyan}ROUTSTRD USAGE MONITOR${COLORS.reset}`;
|
|
35
38
|
const vimIndicator = `${COLORS.yellow}[vim]${COLORS.reset}`;
|
|
36
|
-
const
|
|
39
|
+
const maxKey = visibleTabs.length;
|
|
40
|
+
const help = `${COLORS.dim}[Q] Quit [↑↓] Scroll [←→] Tabs [1-${maxKey}] Tabs [R] Refresh${COLORS.reset}`;
|
|
37
41
|
const fill = width - title.length - help.length - vimIndicator.length - 6;
|
|
38
42
|
return `${title}${vimIndicator}${" ".repeat(Math.max(1, fill))}${help}\n`;
|
|
39
43
|
}
|
|
@@ -49,8 +53,8 @@ export function renderSearchBar(): string {
|
|
|
49
53
|
return `\n${searchLine}${placeholder}\n`;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
export function renderTabs(activeTab: TabId): string {
|
|
53
|
-
const tabStr =
|
|
56
|
+
export function renderTabs(activeTab: TabId, visibleTabs: Tab[]): string {
|
|
57
|
+
const tabStr = visibleTabs.map((tab) => tab.id === activeTab
|
|
54
58
|
? `${COLORS.bgBlue} ${tab.key}:${tab.name} ${COLORS.reset}`
|
|
55
59
|
: `${COLORS.dim}[${tab.key}]${COLORS.reset} ${tab.name}`).join(" ");
|
|
56
60
|
return `${" ".repeat(2)}${tabStr}\n`;
|
|
@@ -507,12 +511,106 @@ export function renderClients(stats: UsageStats, width: number): string {
|
|
|
507
511
|
return output;
|
|
508
512
|
}
|
|
509
513
|
|
|
514
|
+
export function renderNpubs(stats: UsageStats, clients: ClientInfo[], width: number): string {
|
|
515
|
+
const npubStats = getNpubStats(stats.entries, clients);
|
|
516
|
+
if (npubStats.length === 0) return renderBox(["No npub data available"], width, "Npub Breakdown");
|
|
517
|
+
|
|
518
|
+
const totalCost = stats.totalSatsCost;
|
|
519
|
+
const maxCost = npubStats[0]!.satsCost;
|
|
520
|
+
const lines: string[] = [];
|
|
521
|
+
|
|
522
|
+
const col1 = 24; // Npub (truncated)
|
|
523
|
+
const col2 = 12; // Requests
|
|
524
|
+
const col3 = 24; // Cost
|
|
525
|
+
const col4 = 12; // Tokens
|
|
526
|
+
|
|
527
|
+
const hNpub = "Npub".padEnd(col1);
|
|
528
|
+
const hReqs = "Requests".padEnd(col2);
|
|
529
|
+
const hCost = "Cost".padEnd(col3);
|
|
530
|
+
const hTok = "Tokens".padEnd(col4);
|
|
531
|
+
lines.push(`${COLORS.bold}${hNpub}${hReqs}${hCost}${hTok}Avg Cost${COLORS.reset}`);
|
|
532
|
+
lines.push(COLORS.dim + "─".repeat(Math.max(0, width - 4)) + COLORS.reset);
|
|
533
|
+
|
|
534
|
+
startBarSection("npub-detail", col1);
|
|
535
|
+
for (const npub of npubStats) {
|
|
536
|
+
const pct = totalCost > 0 ? ((npub.satsCost / totalCost) * 100).toFixed(1) : "0.0";
|
|
537
|
+
const avgCostFormatted = formatCost(npub.requests > 0 ? npub.satsCost / npub.requests : 0);
|
|
538
|
+
const shortNpub = truncateNpub(npub.npub);
|
|
539
|
+
|
|
540
|
+
const dNpub = shortNpub.padEnd(col1);
|
|
541
|
+
const dReqs = formatReqs(npub.requests).padEnd(col2);
|
|
542
|
+
const dCost = `${formatCost(npub.satsCost)} sats (${pct}%)`.padEnd(col3);
|
|
543
|
+
const dTok = formatNumber(npub.totalTokens).padEnd(col4);
|
|
544
|
+
const dAvg = `${avgCostFormatted} sats/req`;
|
|
545
|
+
|
|
546
|
+
lines.push(
|
|
547
|
+
`${COLORS.magenta}${COLORS.bold}${dNpub}${COLORS.reset}` +
|
|
548
|
+
`${dReqs}` +
|
|
549
|
+
`${COLORS.green}${dCost}${COLORS.reset}` +
|
|
550
|
+
`${COLORS.dim}${dTok}${dAvg}${COLORS.reset}`
|
|
551
|
+
);
|
|
552
|
+
// Full npub on its own line for copy-ability
|
|
553
|
+
lines.push(` ${COLORS.dim}${npub.npub}${COLORS.reset}`);
|
|
554
|
+
lines.push(` ${renderBarChart("", npub.satsCost, maxCost, width - 6, COLORS.magenta, Number(pct), "npub-detail")}`);
|
|
555
|
+
lines.push("");
|
|
556
|
+
}
|
|
557
|
+
endBarSection("npub-detail");
|
|
558
|
+
|
|
559
|
+
let output = renderBox(lines, width, "Npub Breakdown");
|
|
560
|
+
|
|
561
|
+
// Top models per npub (same pattern as clients tab)
|
|
562
|
+
const npubModelMap = new Map<string, Map<string, { requests: number; satsCost: number; tokens: number }>>();
|
|
563
|
+
const clientNpubMap = new Map<string, string>();
|
|
564
|
+
for (const c of clients) {
|
|
565
|
+
if (c.ownerNpub) clientNpubMap.set(c.clientId, c.ownerNpub);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
for (const entry of stats.entries) {
|
|
569
|
+
const npub = clientNpubMap.get(entry.client || "");
|
|
570
|
+
if (!npub) continue;
|
|
571
|
+
const model = entry.modelId;
|
|
572
|
+
if (!npubModelMap.has(npub)) npubModelMap.set(npub, new Map());
|
|
573
|
+
const modelMap = npubModelMap.get(npub)!;
|
|
574
|
+
const existing = modelMap.get(model) || { requests: 0, satsCost: 0, tokens: 0 };
|
|
575
|
+
modelMap.set(model, {
|
|
576
|
+
requests: existing.requests + 1,
|
|
577
|
+
satsCost: existing.satsCost + entry.satsCost,
|
|
578
|
+
tokens: existing.tokens + entry.totalTokens,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const npubModelLines: string[] = [];
|
|
583
|
+
for (const topNpub of npubStats.slice(0, 5)) {
|
|
584
|
+
const modelMap = npubModelMap.get(topNpub.npub);
|
|
585
|
+
if (!modelMap) continue;
|
|
586
|
+
const models = Array.from(modelMap.entries()).sort((a, b) => b[1].satsCost - a[1].satsCost).slice(0, 5);
|
|
587
|
+
npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`);
|
|
588
|
+
for (const [model, data] of models) {
|
|
589
|
+
npubModelLines.push(` ${(MODEL_COLORS[model] || MODEL_COLORS.default)}${model.padEnd(18)}${COLORS.reset} ${formatNumber(data.tokens).padEnd(8)} tokens ${formatCost(data.satsCost)} sats`);
|
|
590
|
+
}
|
|
591
|
+
npubModelLines.push("");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (npubModelLines.length > 0) {
|
|
595
|
+
output += "\n" + renderBox(npubModelLines, width, "Top Models per Npub");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return output;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Truncate an npub for display: first 10 chars + … + last 6 chars. */
|
|
602
|
+
function truncateNpub(npub: string): string {
|
|
603
|
+
if (npub.length <= 24) return npub;
|
|
604
|
+
return npub.slice(0, 10) + "…" + npub.slice(-6);
|
|
605
|
+
}
|
|
606
|
+
|
|
510
607
|
export function renderRecent(stats: UsageStats, width: number): string {
|
|
511
608
|
const recentEntries = stats.entries.slice(0, 50);
|
|
512
609
|
if (recentEntries.length === 0) return renderBox(["No recent entries"], width, "Recent Requests");
|
|
513
610
|
|
|
611
|
+
const clientCol = 14;
|
|
514
612
|
const lines: string[] = [];
|
|
515
|
-
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"TOKENS".padEnd(10)} ${"COST".padEnd(12)} ${"PROVIDER".
|
|
613
|
+
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"TOKENS".padEnd(10)} ${"COST".padEnd(12)} ${"PROVIDER".padEnd(16)} ${"CLIENT".slice(0, clientCol)}${COLORS.reset}`);
|
|
516
614
|
lines.push(COLORS.dim + "─".repeat(width - 4) + COLORS.reset);
|
|
517
615
|
|
|
518
616
|
for (const entry of recentEntries) {
|
|
@@ -520,15 +618,17 @@ export function renderRecent(stats: UsageStats, width: number): string {
|
|
|
520
618
|
const model = entry.modelId.slice(0, 18).padEnd(18);
|
|
521
619
|
const tokens = `${formatNumber(entry.totalTokens).padEnd(6)} (${formatNumber(entry.promptTokens)}+${formatNumber(entry.completionTokens)})`;
|
|
522
620
|
const cost = `${formatCost(entry.satsCost).padEnd(8)} sats`;
|
|
523
|
-
const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0,
|
|
524
|
-
const
|
|
525
|
-
|
|
621
|
+
const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0, 16).padEnd(16);
|
|
622
|
+
const clientName = (entry.client || "unknown").slice(0, clientCol - 1);
|
|
623
|
+
const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white;
|
|
624
|
+
const modelColor = MODEL_COLORS[entry.modelId] || MODEL_COLORS.default;
|
|
625
|
+
lines.push(`${COLORS.dim}${time}${COLORS.reset} ${modelColor}${model}${COLORS.reset} ${tokens.padEnd(10)} ${COLORS.green}${cost}${COLORS.reset} ${COLORS.dim}${provider}${COLORS.reset} ${clientColor}${clientName}${COLORS.reset}`);
|
|
526
626
|
}
|
|
527
627
|
|
|
528
628
|
return renderBox(lines, width, `Recent Requests (${stats.entries.length} shown)`);
|
|
529
629
|
}
|
|
530
630
|
|
|
531
|
-
export function renderTabContent(activeTab: TabId, stats: UsageStats, balance: BalanceInfo | null, status: StatusInfo | null, width: number): string {
|
|
631
|
+
export function renderTabContent(activeTab: TabId, stats: UsageStats, balance: BalanceInfo | null, status: StatusInfo | null, width: number, clients: ClientInfo[] = []): string {
|
|
532
632
|
switch (activeTab) {
|
|
533
633
|
case "overview": return renderOverview(stats, balance, status, width);
|
|
534
634
|
case "today": return renderToday(stats, width);
|
|
@@ -536,6 +636,7 @@ export function renderTabContent(activeTab: TabId, stats: UsageStats, balance: B
|
|
|
536
636
|
case "providers": return renderProviders(stats, width);
|
|
537
637
|
case "tokens": return renderTokens(stats, width);
|
|
538
638
|
case "clients": return renderClients(stats, width);
|
|
639
|
+
case "npubs": return renderNpubs(stats, clients, width);
|
|
539
640
|
case "recent": return renderRecent(stats, width);
|
|
540
641
|
default: return "Unknown tab";
|
|
541
642
|
}
|
package/src/tui/usage/types.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface ClientStats {
|
|
|
44
44
|
totalTokens: number;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export type TabId = "overview" | "today" | "models" | "providers" | "tokens" | "clients" | "recent";
|
|
47
|
+
export type TabId = "overview" | "today" | "models" | "providers" | "tokens" | "clients" | "npubs" | "recent";
|
|
48
48
|
|
|
49
49
|
export interface Tab {
|
|
50
50
|
id: TabId;
|
|
@@ -52,6 +52,15 @@ export interface Tab {
|
|
|
52
52
|
key: string;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export interface NpubStats {
|
|
56
|
+
npub: string;
|
|
57
|
+
requests: number;
|
|
58
|
+
satsCost: number;
|
|
59
|
+
promptTokens: number;
|
|
60
|
+
completionTokens: number;
|
|
61
|
+
totalTokens: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
export interface VimState {
|
|
56
65
|
scrollPos: number;
|
|
57
66
|
searchQuery: string;
|