UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

394 lines (393 loc) • 15 kB
/** * Advanced Analytics Service Layer * Handles metrics collection, aggregation, cost analysis, and projections. */ import { calculateAdvancedCost } from "./pricing.js"; import { InMemoryAnalyticsStorage } from "./storage.js"; export class AnalyticsService { storage; constructor(storage) { this.storage = storage || new InMemoryAnalyticsStorage(); } /** * Helper to parse dynamic time range formats safely */ parseTimeRange(timeRange) { if (!timeRange) { return null; } if (typeof timeRange === "object" && "start" in timeRange && "end" in timeRange) { const startObj = timeRange.start; const endObj = timeRange.end; const start = startObj instanceof Date ? startObj.getTime() : new Date(startObj).getTime(); const end = endObj instanceof Date ? endObj.getTime() : new Date(endObj).getTime(); if (!Number.isNaN(start) && !Number.isNaN(end)) { return { start, end }; } } if (typeof timeRange === "string") { const now = Date.now(); const dayMs = 24 * 3600 * 1000; if (timeRange === "last_24_hours") { return { start: now - dayMs, end: now }; } if (timeRange === "last_7_days") { return { start: now - 7 * dayMs, end: now }; } if (timeRange === "last_30_days") { return { start: now - 30 * dayMs, end: now }; } // Month/quarter bounds use UTC midnight for timezone-stable windows if (timeRange === "current_month") { const d = new Date(); const startUtc = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1); return { start: startUtc, end: now }; } if (timeRange === "current_quarter") { const d = new Date(); const qMonth = Math.floor(d.getUTCMonth() / 3) * 3; const startUtc = Date.UTC(d.getUTCFullYear(), qMonth, 1); return { start: startUtc, end: now }; } } return null; } /** * Capture a request lifecycle record */ async trackRequest(record) { // Preserve explicit costs (including 0). Only price when cost is omitted. const cost = record.cost !== undefined ? record.cost : calculateAdvancedCost(record.model, record.inputTokens, record.outputTokens, record.provider); const fullRecord = { ...record, id: crypto.randomUUID(), cost, }; await this.storage.saveRecord(fullRecord); } /** * getProviderMetrics implementation */ async getProviderMetrics(options = {}) { const allRecords = await this.storage.getRecords(); const range = this.parseTimeRange(options.timeRange); // Filter records const records = allRecords.filter((r) => { if (range && (r.timestamp < range.start || r.timestamp > range.end)) { return false; } if (options.providers && options.providers.length > 0) { if (!options.providers.includes(r.provider)) { return false; } } return true; }); // Compute top-level aggregated metrics let totalLatency = 0; let totalTokens = 0; let inputTokens = 0; let outputTokens = 0; let errors = 0; let totalCost = 0; const requestCount = records.length; // Group by provider const providerMap = {}; for (const r of records) { totalLatency += r.latency; totalTokens += r.totalTokens; inputTokens += r.inputTokens; outputTokens += r.outputTokens; if (r.isError) { errors++; } totalCost += r.cost; if (!providerMap[r.provider]) { providerMap[r.provider] = []; } providerMap[r.provider].push(r); } const averageLatency = requestCount > 0 ? totalLatency / requestCount : 0; const errorRate = requestCount > 0 ? errors / requestCount : 0; // Empty datasets must not report a fabricated 100% success rate const successRate = requestCount > 0 ? 1 - errorRate : 0; const costPerToken = totalTokens > 0 ? totalCost / totalTokens : 0; // Build per-provider results const providers = []; for (const [name, pRecords] of Object.entries(providerMap)) { const pCount = pRecords.length; let pLatency = 0; let pTotalTokens = 0; let pInputTokens = 0; let pOutputTokens = 0; let pErrors = 0; let pCost = 0; for (const r of pRecords) { pLatency += r.latency; pTotalTokens += r.totalTokens; pInputTokens += r.inputTokens; pOutputTokens += r.outputTokens; if (r.isError) { pErrors++; } pCost += r.cost; } const pAvgLatency = pCount > 0 ? pLatency / pCount : 0; const pErrorRate = pCount > 0 ? pErrors / pCount : 0; const pSuccessRate = 1 - pErrorRate; const pCostPerToken = pTotalTokens > 0 ? pCost / pTotalTokens : 0; providers.push({ name, averageLatency: pAvgLatency, averageResponseTime: pAvgLatency, // docs compatibility totalTokens: pTotalTokens, inputTokens: pInputTokens, outputTokens: pOutputTokens, errorRate: pErrorRate, successRate: pSuccessRate, costPerToken: pCostPerToken, totalCost: pCost, requestCount: pCount, }); } return { averageLatency, averageResponseTime: averageLatency, totalTokens, inputTokens, outputTokens, errorRate, successRate, costPerToken, totalCost, requestCount, providers, }; } /** * Helper to format timestamps for date groupings. * Clones the Date so week arithmetic never mutates the caller's value. */ formatGroupDate(timestamp, type) { const d = new Date(timestamp); if (type === "day") { return d.toISOString().split("T")[0]; } if (type === "month") { return d.toISOString().substring(0, 7); } if (type === "week") { // UTC Monday of the containing ISO week — never mutate caller input // and avoid local/UTC drift from mixing getDay()/toISOString(). const clone = new Date(timestamp); const day = clone.getUTCDay(); const diff = clone.getUTCDate() - day + (day === 0 ? -6 : 1); clone.setUTCDate(diff); return clone.toISOString().split("T")[0]; } return d.toISOString().split("T")[0]; } /** * getCostAnalysis implementation */ async getCostAnalysis(options = {}) { const allRecords = await this.storage.getRecords(); const range = this.parseTimeRange(options.timeRange); const records = allRecords.filter((r) => { if (range && (r.timestamp < range.start || r.timestamp > range.end)) { return false; } return true; }); let totalCost = 0; let totalTokens = 0; let inputTokens = 0; let outputTokens = 0; const requestCount = records.length; const groups = {}; const providerGroupsMap = {}; const groupByArr = Array.isArray(options.groupBy) ? options.groupBy : typeof options.groupBy === "string" ? [options.groupBy] : ["provider"]; for (const r of records) { totalCost += r.cost; totalTokens += r.totalTokens; inputTokens += r.inputTokens; outputTokens += r.outputTokens; // Ensure per-provider items are always built for documentation support if (!providerGroupsMap[r.provider]) { providerGroupsMap[r.provider] = { groupKey: r.provider, provider: r.provider, totalCost: 0, costPerToken: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, requestCount: 0, }; } const pg = providerGroupsMap[r.provider]; pg.totalCost += r.cost; pg.inputTokens += r.inputTokens; pg.outputTokens += r.outputTokens; pg.totalTokens += r.totalTokens; pg.requestCount += 1; pg.costPerToken = pg.totalTokens > 0 ? pg.totalCost / pg.totalTokens : 0; // Build specific requested group key const keyParts = []; for (const g of groupByArr) { if (g === "provider") { keyParts.push(r.provider); } else if (g === "model") { keyParts.push(r.model); } else if (g === "user_id" || g === "userId") { keyParts.push(r.userId || "unknown"); } else if (g === "day" || g === "week" || g === "month") { keyParts.push(this.formatGroupDate(r.timestamp, g)); } else { keyParts.push(r.provider); } } const groupKey = keyParts.join("#") || r.provider; if (!groups[groupKey]) { groups[groupKey] = { groupKey, provider: r.provider, model: r.model, userId: r.userId, totalCost: 0, costPerToken: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, requestCount: 0, }; } const gItem = groups[groupKey]; gItem.totalCost += r.cost; gItem.inputTokens += r.inputTokens; gItem.outputTokens += r.outputTokens; gItem.totalTokens += r.totalTokens; gItem.requestCount += 1; gItem.costPerToken = gItem.totalTokens > 0 ? gItem.totalCost / gItem.totalTokens : 0; } const providers = Object.values(providerGroupsMap); // Optional future projections using simple average-based estimation let projections; if (options.includeProjections) { if (records.length === 0) { projections = { nextMonth: 0, nextQuarter: 0 }; } else { // Find span of records in days const timestamps = records.map((r) => r.timestamp); const minTime = Math.min(...timestamps); const maxTime = Math.max(...timestamps); const diffDays = Math.max(1, (maxTime - minTime) / (24 * 3600 * 1000)); const dailyAverage = totalCost / diffDays; projections = { nextMonth: dailyAverage * 30, nextQuarter: dailyAverage * 90, }; } } return { totalCost, totalTokens, inputTokens, outputTokens, requestCount, groups, providers, projections, }; } /** * getTeamAnalytics implementation */ async getTeamAnalytics(options = {}) { const allRecords = await this.storage.getRecords(); const range = this.parseTimeRange(options.timeRange); // teamId and departments are independent AND filters (docs: both supported). const records = allRecords.filter((r) => { if (range && (r.timestamp < range.start || r.timestamp > range.end)) { return false; } if (options.teamId) { const recTeam = r.teamId || "default"; if (recTeam !== options.teamId) { return false; } } if (options.departments && options.departments.length > 0) { const dept = r.department; if (!dept || !options.departments.includes(dept)) { return false; } } return true; }); const totalRequests = records.length; const uniqueUsersSet = new Set(); const providersSet = new Set(); const costBreakdownByProvider = {}; const costBreakdownByUser = {}; let totalQualityScore = 0; let totalRelevance = 0; let totalAccuracy = 0; let totalCompleteness = 0; let scoredCount = 0; for (const r of records) { if (r.userId) { uniqueUsersSet.add(r.userId); } providersSet.add(r.provider); costBreakdownByProvider[r.provider] = (costBreakdownByProvider[r.provider] || 0) + r.cost; const uKey = r.userId || "anonymous"; costBreakdownByUser[uKey] = (costBreakdownByUser[uKey] || 0) + r.cost; if (r.qualityScore) { scoredCount++; totalQualityScore += r.qualityScore.overall; totalRelevance += r.qualityScore.relevance; totalAccuracy += r.qualityScore.accuracy; totalCompleteness += r.qualityScore.completeness; } } // Omit fabricated scores when no requests were evaluated const qualityScores = scoredCount > 0 ? { overall: totalQualityScore / scoredCount, relevance: totalRelevance / scoredCount, accuracy: totalAccuracy / scoredCount, completeness: totalCompleteness / scoredCount, reasoning: "Aggregated automated response evaluations", } : undefined; return { totalRequests, // Count only attributed users — never fabricate a user when none present uniqueUsers: uniqueUsersSet.size, providersUsed: Array.from(providersSet), costBreakdownByProvider, costBreakdownByUser, qualityScores, }; } }