UNPKG

lynkr

Version:

Self-hosted LLM gateway and tier-routing proxy for Claude Code, Cursor, and Codex. Routes across Ollama, AWS Bedrock, OpenRouter, Databricks, Azure OpenAI, llama.cpp, and LM Studio with prompt caching, MCP tools, and 60-80% cost savings.

220 lines (189 loc) 6.24 kB
#!/usr/bin/env node /* eslint-disable no-console */ /** * lynkr usage — print AI spend report from routing telemetry. * * Usage: * lynkr-usage # last 30 days * lynkr-usage --days 7 * lynkr-usage --window 1d * lynkr-usage --window all * lynkr-usage --json # machine-readable * lynkr-usage --flagship gpt-5 # alternative comparison model * lynkr-usage --provider moonshot # filter to one provider */ const path = require("path"); // Make sure config/logger pick up the workspace root process.env.WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || path.resolve(__dirname, ".."); const aggregator = require("../src/usage/aggregator"); function parseArgs(argv) { const opts = { window: "30d", json: false }; for (let i = 2; i < argv.length; i++) { const a = argv[i]; const next = argv[i + 1]; if (a === "--json") opts.json = true; else if (a === "--days" && next) { opts.window = `${parseInt(next, 10)}d`; i++; } else if (a === "--window" && next) { opts.window = next; i++; } else if (a === "--since" && next) { opts.window = next; i++; } else if (a === "--flagship" && next) { opts.flagship = next; i++; } else if (a === "--provider" && next) { opts.provider = next; i++; } else if (a === "--model" && next) { opts.model = next; i++; } else if (a === "--help" || a === "-h") { printHelp(); process.exit(0); } } return opts; } function printHelp() { console.log(`Lynkr usage report — show AI spend and tier-routing savings. Usage: lynkr usage [options] Options: --days N Window in days (e.g. --days 7) --window <preset> Window preset: 1d, 7d, 30d, all (default: 30d) --since <iso> Custom start time (ISO 8601 or epoch ms) --flagship <model> Comparison model for "savings" math (default: claude-sonnet-4-5-20250929) --provider <name> Filter to a single provider --model <id> Filter to a single model --json Print as JSON instead of a formatted table -h, --help Show this help Examples: lynkr usage lynkr usage --days 7 lynkr usage --window all --json `); } const C = { reset: "\x1b[0m", dim: "\x1b[2m", bold: "\x1b[1m", green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m", red: "\x1b[31m", gray: "\x1b[90m", }; function colour(text, code) { if (!process.stdout.isTTY) return text; return `${code}${text}${C.reset}`; } function fmtUSD(n) { if (!n) return "$0.00"; if (n < 0.01) return `$${n.toFixed(4)}`; return `$${n.toFixed(2)}`; } function fmtTokens(n) { if (!n) return "0"; if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return String(n); } function fmtInt(n) { return new Intl.NumberFormat("en-US").format(n || 0); } function pad(s, width, align = "left") { s = String(s); if (s.length >= width) return s; const filler = " ".repeat(width - visibleLength(s)); return align === "right" ? filler + s : s + filler; } function visibleLength(s) { // strip ANSI for column-width math return String(s).replace(/\x1b\[[0-9;]*m/g, "").length; } function tableRow(cells, widths, aligns) { return cells .map((c, i) => pad(c, widths[i], aligns[i] || "left")) .join(" "); } function printTable(rows, header, widths, aligns) { console.log(colour(tableRow(header, widths, aligns), C.bold)); console.log(colour(widths.map((w) => "─".repeat(w)).join(" "), C.dim)); for (const row of rows) { console.log(tableRow(row, widths, aligns)); } } function bucketRows(bucket, widths) { return Object.entries(bucket) .sort((a, b) => b[1].actualCost - a[1].actualCost) .map(([key, b]) => [ key, fmtInt(b.requests), fmtTokens(b.totalTokens), colour(fmtUSD(b.actualCost), C.cyan), colour(fmtUSD(b.flagshipCost), C.gray), colour(fmtUSD(b.saved), C.green), colour(`${b.savedPercent.toFixed(1)}%`, C.green), ]); } function printReport(usage) { const { window, since, flagship, totals, byTier, byProvider, byModel } = usage; const banner = `Lynkr — Usage Report`; console.log(""); console.log(colour(banner, C.bold)); console.log( colour( `window: ${window}${since ? ` since: ${since}` : ""} flagship-comparison: ${flagship}`, C.dim ) ); console.log(""); // Summary line const headline = `${fmtInt(totals.requests)} requests ` + `${fmtTokens(totals.totalTokens)} tokens ` + `actual ${colour(fmtUSD(totals.actualCost), C.cyan)} ` + `flagship-only ${colour(fmtUSD(totals.flagshipCost), C.gray)} ` + `saved ${colour(fmtUSD(totals.saved), C.green)} ` + colour(`(${totals.savedPercent.toFixed(1)}%)`, C.green); console.log(headline); if (totals.fallbacks || totals.errors) { console.log( colour( ` ${totals.fallbacks} fallback${totals.fallbacks !== 1 ? "s" : ""}, ` + `${totals.errors} error${totals.errors !== 1 ? "s" : ""}`, C.yellow ) ); } console.log(""); if (totals.requests === 0) { console.log(colour("No telemetry yet for this window. Send some requests through Lynkr first.", C.yellow)); return; } const headers = ["", "REQUESTS", "TOKENS", "ACTUAL", "FLAGSHIP", "SAVED", "PCT"]; const widths = [22, 9, 9, 10, 10, 10, 7]; const aligns = ["left", "right", "right", "right", "right", "right", "right"]; console.log(colour("BY TIER", C.bold)); printTable(bucketRows(byTier, widths), ["TIER", ...headers.slice(1)], widths, aligns); console.log(""); console.log(colour("BY PROVIDER", C.bold)); printTable(bucketRows(byProvider, widths), ["PROVIDER", ...headers.slice(1)], widths, aligns); console.log(""); console.log(colour("BY MODEL", C.bold)); printTable(bucketRows(byModel, widths), ["MODEL", ...headers.slice(1)], widths, aligns); console.log(""); } function main() { const opts = parseArgs(process.argv); const usage = aggregator.getUsage(opts); if (opts.json) { process.stdout.write(JSON.stringify(usage, null, 2) + "\n"); return; } printReport(usage); } main();