UNPKG

spinal-obs-node

Version:

WithSpinal cost-aware OpenTelemetry SDK for Node.js

553 lines (550 loc) 31.2 kB
#!/usr/bin/env node import { Analytics } from "../chunk-QPAMBULF.js"; import { getConfig } from "../chunk-7N3YU2IZ.js"; import { estimateCost } from "../chunk-GCDHWKI2.js"; // src/cli/index.ts import { Command as Command2 } from "commander"; import Conf from "conf"; import open from "open"; import fs from "fs"; // src/cli/analytics.ts import { Command } from "commander"; function createAnalyticsCommands() { const commands = []; const costCommand = new Command("cost").description("Analyze OpenAI API costs").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--by-model", "Show breakdown by model").option("--by-aggregation", "Show breakdown by aggregation ID").option("--trends", "Show cost trends").action(async (options) => { const config = getConfig(); const analytics = new Analytics(config.localStorePath); const analysis = analytics.analyzeCosts({ since: options.since, format: options.format, byModel: options.byModel, byAggregation: options.byAggregation, trends: options.trends }); if (options.format === "json") { console.log(JSON.stringify(analysis, null, 2)); } else if (options.format === "csv") { console.log("Period,Total Cost,Total Calls,Average Cost Per Call"); console.log(`${options.since},${analysis.totalCost.toFixed(6)},${analysis.totalCalls},${analysis.averageCostPerCall.toFixed(6)}`); } else if (options.format === "summary") { console.log(`\u{1F4B0} Total Cost: $${analysis.totalCost.toFixed(4)}`); console.log(`\u{1F4CA} Total Calls: ${analysis.totalCalls}`); console.log(`\u{1F4C8} Average Cost per Call: $${analysis.averageCostPerCall.toFixed(6)}`); } else { console.log(` \u{1F4B0} Cost Analysis (Last ${options.since}) `); console.log("\u2500".repeat(60)); console.log(`Total Cost: $${analysis.totalCost.toFixed(4)}`); console.log(`Total API Calls: ${analysis.totalCalls}`); console.log(`Average Cost per Call: $${analysis.averageCostPerCall.toFixed(6)}`); console.log("\u2500".repeat(60)); if (options.byModel && Object.keys(analysis.costByModel).length > 0) { console.log("\n\u{1F4CA} Cost by Model:"); Object.entries(analysis.costByModel).forEach(([model, data]) => { console.log(`\u2022 ${model}: $${data.cost.toFixed(4)} (${data.calls} calls, ${data.percentage.toFixed(1)}%)`); }); } if (options.byAggregation && Object.keys(analysis.costByAggregation).length > 0) { console.log("\n\u{1F3F7}\uFE0F Cost by Aggregation:"); Object.entries(analysis.costByAggregation).forEach(([agg, data]) => { console.log(`\u2022 ${agg}: $${data.cost.toFixed(4)} (${data.calls} calls, ${data.percentage.toFixed(1)}%)`); }); } if (options.trends && analysis.costTrends.length > 0) { console.log("\n\u{1F4C8} Cost Trends:"); analysis.costTrends.forEach((trend) => { console.log(`\u2022 ${trend.date}: $${trend.cost.toFixed(4)} (${trend.calls} calls)`); }); } } }); const usageCommand = new Command("usage").description("Analyze OpenAI API usage patterns").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "24h").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--tokens", "Show token breakdown").option("--by-model", "Show usage by model").option("--by-aggregation", "Show usage by aggregation ID").action(async (options) => { const config = getConfig(); const analytics = new Analytics(config.localStorePath); const analysis = analytics.analyzeUsage({ since: options.since, format: options.format, byModel: options.byModel, byAggregation: options.byAggregation }); if (options.format === "json") { console.log(JSON.stringify(analysis, null, 2)); } else if (options.format === "csv") { console.log("Period,Total Calls,Total Tokens,Input Tokens,Output Tokens"); console.log(`${options.since},${analysis.totalCalls},${analysis.totalTokens},${analysis.inputTokens},${analysis.outputTokens}`); } else if (options.format === "summary") { console.log(`\u{1F4CA} Total Calls: ${analysis.totalCalls}`); console.log(`\u{1F524} Total Tokens: ${analysis.totalTokens.toLocaleString()}`); console.log(`\u{1F4E5} Input Tokens: ${analysis.inputTokens.toLocaleString()}`); console.log(`\u{1F4E4} Output Tokens: ${analysis.outputTokens.toLocaleString()}`); } else { console.log(` \u{1F4CA} Usage Analytics (Last ${options.since}) `); console.log("\u2500".repeat(60)); console.log(`Total API Calls: ${analysis.totalCalls}`); console.log(`Total Tokens: ${analysis.totalTokens.toLocaleString()}`); console.log(`\u2022 Input tokens: ${analysis.inputTokens.toLocaleString()}`); console.log(`\u2022 Output tokens: ${analysis.outputTokens.toLocaleString()}`); console.log("\u2500".repeat(60)); if (options.tokens) { console.log("\n\u{1F4C8} Token Efficiency:"); console.log(`\u2022 Average input tokens per call: ${analysis.tokenEfficiency.averageInputTokensPerCall.toFixed(1)}`); console.log(`\u2022 Average output tokens per call: ${analysis.tokenEfficiency.averageOutputTokensPerCall.toFixed(1)}`); console.log(`\u2022 Token ratio (output/input): ${analysis.tokenEfficiency.tokenRatio.toFixed(2)}`); } if (options.byModel && Object.keys(analysis.usageByModel).length > 0) { console.log("\n\u{1F916} Usage by Model:"); Object.entries(analysis.usageByModel).forEach(([model, data]) => { console.log(`\u2022 ${model}: ${data.calls} calls (${data.percentage.toFixed(1)}%), ${data.tokens.toLocaleString()} tokens`); }); } if (options.byAggregation && Object.keys(analysis.usageByAggregation).length > 0) { console.log("\n\u{1F3F7}\uFE0F Usage by Aggregation:"); Object.entries(analysis.usageByAggregation).forEach(([agg, data]) => { console.log(`\u2022 ${agg}: ${data.calls} calls (${data.percentage.toFixed(1)}%), ${data.tokens.toLocaleString()} tokens`); }); } } }); const performanceCommand = new Command("performance").description("Analyze OpenAI API performance").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--response-times", "Show response time analysis").option("--errors", "Show error analysis").option("--by-model", "Show performance by model").action(async (options) => { const config = getConfig(); const analytics = new Analytics(config.localStorePath); const analysis = analytics.analyzePerformance({ since: options.since, format: options.format }); if (options.format === "json") { console.log(JSON.stringify(analysis, null, 2)); } else if (options.format === "csv") { console.log("Period,Total Requests,Successful,Failed,Success Rate,Avg Response Time"); console.log(`${options.since},${analysis.totalRequests},${analysis.successful},${analysis.failed},${analysis.successRate.toFixed(1)}%,${analysis.responseTimes.average.toFixed(1)}ms`); } else if (options.format === "summary") { console.log(`\u26A1 Total Requests: ${analysis.totalRequests}`); console.log(`\u2705 Successful: ${analysis.successful} (${analysis.successRate.toFixed(1)}%)`); console.log(`\u274C Failed: ${analysis.failed}`); console.log(`\u23F1\uFE0F Avg Response Time: ${analysis.responseTimes.average.toFixed(1)}ms`); } else { console.log(` \u26A1 Performance Analytics (Last ${options.since}) `); console.log("\u2500".repeat(60)); console.log(`Total Requests: ${analysis.totalRequests}`); console.log(`Successful: ${analysis.successful} (${analysis.successRate.toFixed(1)}%)`); console.log(`Failed: ${analysis.failed} (${(100 - analysis.successRate).toFixed(1)}%)`); console.log("\u2500".repeat(60)); if (options.responseTimes) { console.log("\n\u23F1\uFE0F Response Times:"); console.log(`\u2022 Average: ${analysis.responseTimes.average.toFixed(1)}ms`); console.log(`\u2022 Median: ${analysis.responseTimes.median.toFixed(1)}ms`); console.log(`\u2022 95th percentile: ${analysis.responseTimes.p95.toFixed(1)}ms`); console.log(`\u2022 Fastest: ${analysis.responseTimes.fastest.toFixed(1)}ms`); console.log(`\u2022 Slowest: ${analysis.responseTimes.slowest.toFixed(1)}ms`); } if (options.errors && analysis.errors.other > 0) { console.log("\n\u{1F6A8} Error Analysis:"); console.log(`\u2022 Rate limit errors: ${analysis.errors.rateLimit}`); console.log(`\u2022 Authentication errors: ${analysis.errors.authentication}`); console.log(`\u2022 Network errors: ${analysis.errors.network}`); console.log(`\u2022 Other errors: ${analysis.errors.other}`); } } }); const modelsCommand = new Command("models").description("Compare performance and costs across different models").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "30d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--efficiency", "Show model efficiency metrics").option("--costs", "Show cost analysis by model").action(async (options) => { const config = getConfig(); const analytics = new Analytics(config.localStorePath); const analysis = analytics.analyzeModels({ since: options.since, format: options.format }); if (options.format === "json") { console.log(JSON.stringify(analysis, null, 2)); } else if (options.format === "summary") { Object.entries(analysis.models).forEach(([model, data]) => { console.log(`${model}: ${data.calls} calls, $${data.totalCost.toFixed(4)}, ${data.successRate.toFixed(1)}% success`); }); } else { console.log(` \u{1F916} Model Analytics (Last ${options.since}) `); console.log("\u2500".repeat(80)); Object.entries(analysis.models).forEach(([model, data]) => { console.log(`${model}:`); console.log(` \u2022 Calls: ${data.calls}`); console.log(` \u2022 Total cost: $${data.totalCost.toFixed(4)}`); console.log(` \u2022 Avg cost per call: $${data.avgCostPerCall.toFixed(6)}`); console.log(` \u2022 Avg response time: ${data.avgResponseTime.toFixed(1)}ms`); console.log(` \u2022 Success rate: ${data.successRate.toFixed(1)}%`); console.log(` \u2022 Total tokens: ${data.totalTokens.toLocaleString()}`); console.log(""); }); } }); const aggregationsCommand = new Command("aggregations").description("Analyze usage by custom aggregation IDs").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--id <aggregationId>", "Show specific aggregation ID").option("--costs", "Show cost analysis by aggregation").action(async (options) => { const config = getConfig(); const analytics = new Analytics(config.localStorePath); const analysis = analytics.analyzeAggregations({ since: options.since, format: options.format }); if (options.format === "json") { console.log(JSON.stringify(analysis, null, 2)); } else if (options.format === "summary") { Object.entries(analysis.aggregations).forEach(([agg, data]) => { console.log(`${agg}: ${data.calls} calls, $${data.totalCost.toFixed(4)}, ${data.successRate.toFixed(1)}% success`); }); } else { console.log(` \u{1F3F7}\uFE0F Aggregation Analytics (Last ${options.since}) `); console.log("\u2500".repeat(80)); Object.entries(analysis.aggregations).forEach(([agg, data]) => { console.log(`${agg}:`); console.log(` \u2022 Calls: ${data.calls}`); console.log(` \u2022 Total cost: $${data.totalCost.toFixed(4)}`); console.log(` \u2022 Avg cost per call: $${data.avgCostPerCall.toFixed(6)}`); console.log(` \u2022 Success rate: ${data.successRate.toFixed(1)}%`); console.log(` \u2022 Total tokens: ${data.totalTokens.toLocaleString()}`); console.log(""); }); } }); const trendsCommand = new Command("trends").description("Identify usage patterns and trends over time").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "30d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--costs", "Show cost trends").option("--usage", "Show usage trends").option("--performance", "Show performance trends").action(async (options) => { const config = getConfig(); const analytics = new Analytics(config.localStorePath); const analysis = analytics.analyzeTrends({ since: options.since, format: options.format }); if (options.format === "json") { console.log(JSON.stringify(analysis, null, 2)); } else if (options.format === "summary") { console.log(`\u{1F4CA} Daily avg calls: ${analysis.usageTrends.dailyAverageCalls.toFixed(1)}`); console.log(`\u{1F4B0} Daily avg cost: $${analysis.costTrends.dailyAverageCost.toFixed(4)}`); console.log(`\u26A1 Performance: ${analysis.performanceTrends.responseTimeTrend}`); } else { console.log(` \u{1F4C8} Trends Analysis (Last ${options.since}) `); console.log("\u2500".repeat(60)); if (options.usage || !options.costs && !options.performance) { console.log("\n\u{1F4CA} Usage Trends:"); console.log(`\u2022 Daily average calls: ${analysis.usageTrends.dailyAverageCalls.toFixed(1)}`); console.log(`\u2022 Peak usage: ${analysis.usageTrends.peakUsage.calls} calls (${analysis.usageTrends.peakUsage.date})`); console.log(`\u2022 Growth rate: ${analysis.usageTrends.growthRate > 0 ? "+" : ""}${analysis.usageTrends.growthRate.toFixed(1)}% week-over-week`); } if (options.costs || !options.usage && !options.performance) { console.log("\n\u{1F4B0} Cost Trends:"); console.log(`\u2022 Daily average cost: $${analysis.costTrends.dailyAverageCost.toFixed(4)}`); console.log(`\u2022 Peak cost: $${analysis.costTrends.peakCost.cost.toFixed(4)} (${analysis.costTrends.peakCost.date})`); console.log(`\u2022 Cost per call trend: ${analysis.costTrends.costPerCallTrend}`); } if (options.performance || !options.usage && !options.costs) { console.log("\n\u26A1 Performance Trends:"); console.log(`\u2022 Response time trend: ${analysis.performanceTrends.responseTimeTrend}`); console.log(`\u2022 Error rate trend: ${analysis.performanceTrends.errorRateTrend}`); console.log(`\u2022 Success rate trend: ${analysis.performanceTrends.successRateTrend}`); } } }); const optimizeCommand = new Command("optimize").description("Get actionable recommendations to optimize costs and performance").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--costs", "Show cost optimization recommendations").option("--performance", "Show performance optimization recommendations").action(async (options) => { const config = getConfig(); const analytics = new Analytics(config.localStorePath); const recommendations = analytics.getOptimizationRecommendations({ since: options.since, format: options.format }); if (options.format === "json") { console.log(JSON.stringify(recommendations, null, 2)); } else if (options.format === "summary") { const allRecs = [ ...recommendations.costOptimization, ...recommendations.performanceOptimization, ...recommendations.usageOptimization ]; allRecs.forEach((rec) => console.log(`\u2022 ${rec}`)); } else { console.log(` \u{1F4A1} Optimization Recommendations (Last ${options.since}) `); console.log("\u2500".repeat(60)); if (options.costs || !options.performance && recommendations.costOptimization.length > 0) { console.log("\n\u{1F4B0} Cost Optimization:"); recommendations.costOptimization.forEach((rec) => { console.log(`\u2022 ${rec}`); }); } if (options.performance || !options.costs && recommendations.performanceOptimization.length > 0) { console.log("\n\u26A1 Performance Optimization:"); recommendations.performanceOptimization.forEach((rec) => { console.log(`\u2022 ${rec}`); }); } if (!options.costs && !options.performance && recommendations.usageOptimization.length > 0) { console.log("\n\u{1F3AF} Usage Optimization:"); recommendations.usageOptimization.forEach((rec) => { console.log(`\u2022 ${rec}`); }); } if (recommendations.costOptimization.length === 0 && recommendations.performanceOptimization.length === 0 && recommendations.usageOptimization.length === 0) { console.log("\n\u2705 No specific optimization recommendations at this time."); console.log("Your OpenAI API usage appears to be well-optimized!"); } } }); const responsesCommand = new Command("responses").description("Analyze OpenAI API response content and quality").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--errors", "Show detailed error analysis").option("--by-model", "Show response quality by model").option("--size-distribution", "Show response size distribution").action(async (options) => { const config = getConfig(); const analytics = new Analytics(config.localStorePath); const analysis = analytics.analyzeResponses({ since: options.since, format: options.format }); if (options.format === "json") { console.log(JSON.stringify(analysis, null, 2)); } else if (options.format === "csv") { console.log("Period,Total Responses,Avg Size,Success Rate,Error Rate"); console.log(`${options.since},${analysis.totalResponses},${analysis.averageResponseSize.toFixed(1)},${analysis.errorAnalysis.successRate.toFixed(1)}%,${analysis.errorAnalysis.totalErrors}`); } else if (options.format === "summary") { console.log(`\u{1F4C4} Total Responses: ${analysis.totalResponses}`); console.log(`\u{1F4CF} Avg Response Size: ${analysis.averageResponseSize.toFixed(1)} bytes`); console.log(`\u2705 Success Rate: ${analysis.errorAnalysis.successRate.toFixed(1)}%`); console.log(`\u274C Total Errors: ${analysis.errorAnalysis.totalErrors}`); } else { console.log(` \u{1F4C4} Response Analysis (Last ${options.since}) `); console.log("\u2500".repeat(60)); console.log(`Total Responses: ${analysis.totalResponses}`); console.log(`Average Response Size: ${analysis.averageResponseSize.toFixed(1)} bytes`); console.log(`Success Rate: ${analysis.errorAnalysis.successRate.toFixed(1)}%`); console.log(`Error Rate: ${analysis.errorAnalysis.totalErrors > 0 ? (analysis.errorAnalysis.totalErrors / analysis.totalResponses * 100).toFixed(1) : 0}%`); console.log("\u2500".repeat(60)); if (options.sizeDistribution) { console.log("\n\u{1F4CA} Response Size Distribution:"); console.log(`\u2022 Small (< 500 bytes): ${analysis.responseSizeDistribution.small} responses`); console.log(`\u2022 Medium (500-2000 bytes): ${analysis.responseSizeDistribution.medium} responses`); console.log(`\u2022 Large (> 2000 bytes): ${analysis.responseSizeDistribution.large} responses`); } if (options.errors && analysis.errorAnalysis.totalErrors > 0) { console.log("\n\u{1F6A8} Error Analysis:"); Object.entries(analysis.errorAnalysis.errorTypes).forEach(([errorType, count]) => { console.log(`\u2022 ${errorType}: ${count} occurrences`); }); if (analysis.errorAnalysis.errorMessages.length > 0) { console.log("\n\u{1F4DD} Recent Error Messages:"); analysis.errorAnalysis.errorMessages.slice(0, 5).forEach((msg) => { console.log(`\u2022 ${msg}`); }); } } if (options.byModel && Object.keys(analysis.modelResponseQuality).length > 0) { console.log("\n\u{1F916} Response Quality by Model:"); Object.entries(analysis.modelResponseQuality).forEach(([model, data]) => { console.log(`${model}:`); console.log(` \u2022 Avg response length: ${data.averageResponseLength.toFixed(1)} chars`); console.log(` \u2022 Avg response size: ${data.averageResponseSize.toFixed(1)} bytes`); console.log(` \u2022 Success rate: ${data.successRate.toFixed(1)}%`); console.log(""); }); } } }); const contentCommand = new Command("content").description("Get insights about response content patterns and quality").option("--since <period>", "Time period (1h, 24h, 7d, 30d, 90d, 1y)", "7d").option("--format <format>", "Output format (table, json, csv, summary)", "table").option("--patterns", "Show response length patterns").option("--finish-reasons", "Show finish reason distribution").option("--quality", "Show response quality metrics").action(async (options) => { const config = getConfig(); const analytics = new Analytics(config.localStorePath); const insights = analytics.getContentInsights({ since: options.since, format: options.format }); if (options.format === "json") { console.log(JSON.stringify(insights, null, 2)); } else if (options.format === "summary") { console.log(`\u{1F4CA} Response Patterns: ${insights.responsePatterns.shortResponses + insights.responsePatterns.mediumResponses + insights.responsePatterns.longResponses} total`); console.log(`\u{1F3AF} Avg tokens/char: ${insights.responseQuality.averageTokensPerCharacter.toFixed(2)}`); console.log(`\u26A1 Response efficiency: ${insights.responseQuality.responseEfficiency.toFixed(4)}`); } else { console.log(` \u{1F4DD} Content Insights (Last ${options.since}) `); console.log("\u2500".repeat(60)); if (options.patterns || !options.finishReasons && !options.quality) { console.log("\n\u{1F4CA} Response Length Patterns:"); const total = insights.responsePatterns.shortResponses + insights.responsePatterns.mediumResponses + insights.responsePatterns.longResponses; console.log(`\u2022 Short responses (< 50 chars): ${insights.responsePatterns.shortResponses} (${total > 0 ? (insights.responsePatterns.shortResponses / total * 100).toFixed(1) : 0}%)`); console.log(`\u2022 Medium responses (50-200 chars): ${insights.responsePatterns.mediumResponses} (${total > 0 ? (insights.responsePatterns.mediumResponses / total * 100).toFixed(1) : 0}%)`); console.log(`\u2022 Long responses (> 200 chars): ${insights.responsePatterns.longResponses} (${total > 0 ? (insights.responsePatterns.longResponses / total * 100).toFixed(1) : 0}%)`); } if (options.finishReasons || !options.patterns && !options.quality) { console.log("\n\u{1F3AF} Finish Reasons:"); Object.entries(insights.finishReasons).forEach(([reason, count]) => { console.log(`\u2022 ${reason}: ${count} responses`); }); } if (options.quality || !options.patterns && !options.finishReasons) { console.log("\n\u26A1 Response Quality Metrics:"); console.log(`\u2022 Average tokens per character: ${insights.responseQuality.averageTokensPerCharacter.toFixed(2)}`); console.log(`\u2022 Response efficiency (tokens/byte): ${insights.responseQuality.responseEfficiency.toFixed(4)}`); } if (Object.values(insights.commonErrors).some((count) => count > 0)) { console.log("\n\u{1F6A8} Common Error Types:"); Object.entries(insights.commonErrors).forEach(([errorType, count]) => { if (count > 0) { console.log(`\u2022 ${errorType}: ${count} occurrences`); } }); } } }); commands.push( costCommand, usageCommand, performanceCommand, modelsCommand, aggregationsCommand, trendsCommand, optimizeCommand, responsesCommand, contentCommand ); return commands; } // src/cli/index.ts var program = new Command2(); program.name("spinal").description("Spinal CLI").version("0.1.0"); var store = new Conf({ projectName: "spinal" }); program.command("status").description("Show current mode and configuration summary").action(async () => { const cfg = getConfig(); const mode = cfg.mode; const endpoint = cfg.endpoint; const excluded = process.env.SPINAL_EXCLUDED_HOSTS ?? "api.anthropic.com,api.azure.com"; const excludeOpenAI = process.env.SPINAL_EXCLUDE_OPENAI === "true"; console.log(JSON.stringify({ mode, endpoint, localStorePath: cfg.localStorePath, excludedHosts: excluded, excludeOpenAI }, null, 2)); }); program.command("login").description("Login for cloud mode (opens backend dashboard)").option("--dashboard-url <url>", "Backend dashboard URL", "https://dashboard.withspinal.com/login").action(async (opts) => { await open(opts.dashboardUrl); console.log("Opened browser for login. After obtaining an API key, set SPINAL_API_KEY."); }); program.command("init").description("Initialize configuration (optional)").option("--endpoint <url>", "Spinal endpoint", process.env.SPINAL_TRACING_ENDPOINT || "https://cloud.withspinal.com").action(async (opts) => { store.set("endpoint", opts.endpoint); console.log("Saved endpoint to local config."); }); program.command("local").description("Display local collected data in a readable format").option("--limit <number>", "Limit number of spans to display", "10").option("--format <format>", "Output format: table, json, or summary", "table").action(async (opts) => { const cfg = getConfig(); const file = cfg.localStorePath; if (!fs.existsSync(file)) { console.log("No local data found. Start your application with Spinal configured to collect data."); return; } const raw = await fs.promises.readFile(file, "utf8"); const lines = raw.trim().length ? raw.trim().split("\n") : []; if (lines.length === 0) { console.log("No spans collected yet. Start your application with Spinal configured to collect data."); return; } const spans = []; for (const line of lines) { try { const span = JSON.parse(line); spans.push(span); } catch { } } const limit = parseInt(opts.limit, 10); const displaySpans = spans.slice(-limit); if (opts.format === "json") { console.log(JSON.stringify(displaySpans, null, 2)); } else if (opts.format === "summary") { const summary = { totalSpans: spans.length, uniqueTraces: new Set(spans.map((s) => s.trace_id)).size, spanTypes: spans.reduce((acc, span) => { const type = span.name || "unknown"; acc[type] = (acc[type] || 0) + 1; return acc; }, {}), estimatedCost: spans.reduce((total, span) => { const attrs = span.attributes || {}; const inputTokens = Number(attrs["spinal.input_tokens"] || 0); const outputTokens = Number(attrs["spinal.output_tokens"] || 0); const model = String(attrs["spinal.model"] || "openai:gpt-4o-mini"); return total + estimateCost({ model, inputTokens, outputTokens }); }, 0) }; console.log(JSON.stringify(summary, null, 2)); } else { console.log(` \u{1F4CA} Spinal Local Data (showing last ${displaySpans.length} of ${spans.length} spans) `); console.log("\u2500".repeat(120)); console.log(`${"Name".padEnd(30)} ${"Trace ID".padEnd(32)} ${"Duration (ms)".padEnd(12)} ${"Status".padEnd(8)} ${"Model".padEnd(15)} ${"Cost ($)".padEnd(8)}`); console.log("\u2500".repeat(120)); for (const span of displaySpans) { const name = (span.name || "unknown").substring(0, 29).padEnd(30); const traceId = span.trace_id.substring(0, 31).padEnd(32); const duration = span.end_time && span.start_time ? ((span.end_time - span.start_time) / 1e6).toFixed(1).padEnd(12) : "N/A".padEnd(12); const status = String(span.status?.code || "UNSET").padEnd(8); const attrs = span.attributes || {}; const model = (attrs["spinal.model"] || "N/A").toString().substring(0, 14).padEnd(15); const inputTokens = Number(attrs["spinal.input_tokens"] || 0); const outputTokens = Number(attrs["spinal.output_tokens"] || 0); const cost = inputTokens > 0 || outputTokens > 0 ? estimateCost({ model: String(attrs["spinal.model"] || "openai:gpt-4o-mini"), inputTokens, outputTokens }).toFixed(4).padEnd(8) : "N/A".padEnd(8); console.log(`${name} ${traceId} ${duration} ${status} ${model} ${cost}`); } console.log("\u2500".repeat(120)); const totalCost = spans.reduce((total, span) => { const attrs = span.attributes || {}; const inputTokens = Number(attrs["spinal.input_tokens"] || 0); const outputTokens = Number(attrs["spinal.output_tokens"] || 0); const model = String(attrs["spinal.model"] || "openai:gpt-4o-mini"); return total + estimateCost({ model, inputTokens, outputTokens }); }, 0); console.log(` \u{1F4B0} Total estimated cost: $${totalCost.toFixed(4)}`); console.log(`\u{1F4C8} Total spans collected: ${spans.length}`); console.log(`\u{1F50D} Unique traces: ${new Set(spans.map((s) => s.trace_id)).size}`); } }); program.command("report").description("Summarize local usage and estimated costs").option("--since <duration>", "Time window, e.g., 24h", "24h").action(async () => { const cfg = getConfig(); if (cfg.mode !== "local") { console.log("report is for local mode. Set SPINAL_MODE=local or omit SPINAL_API_KEY."); return; } const file = cfg.localStorePath; if (!fs.existsSync(file)) { console.log("No local data found."); return; } const raw = await fs.promises.readFile(file, "utf8"); const lines = raw.trim().length ? raw.trim().split("\n") : []; let count = 0; let est = 0; for (const line of lines) { try { const span = JSON.parse(line); const attrs = span.attributes || {}; const inputTokens = Number(attrs["spinal.input_tokens"] || 0); const outputTokens = Number(attrs["spinal.output_tokens"] || 0); const model = String(attrs["spinal.model"] || "openai:gpt-4o-mini"); est += estimateCost({ model, inputTokens, outputTokens }); count++; } catch { } } console.log(JSON.stringify({ spansProcessed: count, estimatedCostUSD: Number(est.toFixed(4)) }, null, 2)); }); var analyticsCommands = createAnalyticsCommands(); analyticsCommands.forEach((cmd) => program.addCommand(cmd)); program.parseAsync(); //# sourceMappingURL=index.js.map